Compare commits
105 Commits
v9.1.3
..
authserver
| Author | SHA1 | Date | |
|---|---|---|---|
| cfe7bb53e6 | |||
| b2ca6206cc | |||
| 918c2f8587 | |||
| 8f851164d6 | |||
| d215d1998f | |||
| 75e3256497 | |||
| 58f5a17a83 | |||
| e7c3d797be | |||
| 34abd5b8f5 | |||
| 8b138d14bb | |||
| e23abd69b5 | |||
| 9c16ad456d | |||
| 4b851afc6a | |||
| f333148afa | |||
| 8d0160a3e7 | |||
| 4a02e988c1 | |||
| 134472cd4b | |||
| b40a10da7b | |||
| 25f5b33d17 | |||
| f57c39bba2 | |||
| 99b234eca8 | |||
| 9c3c8cc9d1 | |||
| b08e3a5128 | |||
| e48cdc85f7 | |||
| a5da68a7f9 | |||
| 7d594ab0d3 | |||
| 9ed3d668ee | |||
| 0da0a5e027 | |||
| 28eb0b65f4 | |||
| 1d29572ecd | |||
| 07e8d242d1 | |||
| 1586a286d8 | |||
| 4859059eba | |||
| f2949c1836 | |||
| cd6acfb91d | |||
| 2d5dc9a6aa | |||
| 87e7da2aff | |||
| 461eb38d88 | |||
| ba0bb62fa3 | |||
| 1ca62dd38e | |||
| 1b1328c601 | |||
| 9633036887 | |||
| e3d76ea9f4 | |||
| d7212e69b5 | |||
| ead58bd6f6 | |||
| fbe13b75df | |||
| 6085a8231f | |||
| e15cd190b3 | |||
| 3d55423deb | |||
| f62df52c1d | |||
| 7829f94ac4 | |||
| e9d42b9cdd | |||
| 1f05a8d92a | |||
| 69ae2b2997 | |||
| b86e47de02 | |||
| ea7647f43c | |||
| ae7df52780 | |||
| bc5737b9b0 | |||
| d0745d1914 | |||
| 2b4c926a70 | |||
| d922c1c80f | |||
| 67500a7689 | |||
| 1c8aa7440c | |||
| d128dbec4c | |||
| 676cb8810b | |||
| 189e3d5599 | |||
| 009d0b39f9 | |||
| 81a8aa7c3d | |||
| 6c6761d14b | |||
| 7d2e3df929 | |||
| f334c696cb | |||
| db974d72d5 | |||
| c15e342bb8 | |||
| dc1449c7b6 | |||
| 0b305caf58 | |||
| 8f1f3645b2 | |||
| 0079162efe | |||
| 7afec06d4c | |||
| 29f85a8fd2 | |||
| 6e0dc24eca | |||
| cee1180aa7 | |||
| 6db2b55e63 | |||
| a3c038781f | |||
| 59c9e5397e | |||
| a4c253b9a9 | |||
| f12b4faf34 | |||
| ff49759f42 | |||
| 01d0c738bc | |||
| d57554a48c | |||
| b16b57f38b | |||
| 12177446a2 | |||
| 61b15db958 | |||
| 349e8f5139 | |||
| f30482808b | |||
| 79cdecdff6 | |||
| 336dee53cd | |||
| 77022bbd7f | |||
| df96df776d | |||
| 67bc803859 | |||
| 8ef56c6d91 | |||
| d377d1e1cf | |||
| 4209e4d90d | |||
| 83c85d02ee | |||
| 866b72d029 | |||
| 4bc0f44789 |
@@ -4,3 +4,5 @@ installer/src/certs/server.key
|
||||
# vim swap files
|
||||
*.swp
|
||||
|
||||
.cursor
|
||||
|
||||
|
||||
@@ -3169,3 +3169,53 @@
|
||||
* backupintegrity: add percent progress
|
||||
* apps: fix acl display
|
||||
|
||||
[9.1.4]
|
||||
* services: lazy start services / on demand services
|
||||
* restore: fix restore of trusted ips and blocklist
|
||||
* dashboard: wait for dashboard reload when version has changed
|
||||
* graphite: fix aggregation of block/network read/write
|
||||
* Workaround chrome quirks on file drop handling
|
||||
* notifications: add empty text, progress bar and inifinite scroll
|
||||
* rsync: throttle log messages during download
|
||||
* backup logs: make them much terse and concise
|
||||
* oidc: implement Device Authorization Grant
|
||||
* operator: fix viewing of backup progress and logs
|
||||
* notification: automatic app update failure notification
|
||||
* backup sites: identify conflicting site locations
|
||||
* update: add policy to update apps and platform separately
|
||||
* passkey: fix issue where passkeys were lost on restart
|
||||
* passkey: implement passwordless login
|
||||
* oidcserver: fix jwks_rsaonly response
|
||||
|
||||
[9.1.5]
|
||||
* services: lazy start services / on demand services
|
||||
* restore: fix restore of trusted ips and blocklist
|
||||
* dashboard: wait for dashboard reload when version has changed
|
||||
* graphite: fix aggregation of block/network read/write
|
||||
* Workaround chrome quirks on file drop handling
|
||||
* notifications: add empty text, progress bar and inifinite scroll
|
||||
* rsync: throttle log messages during download
|
||||
* backup logs: make them much terse and concise
|
||||
* oidc: implement Device Authorization Grant
|
||||
* operator: fix viewing of backup progress and logs
|
||||
* notification: automatic app update failure notification
|
||||
* backup sites: identify conflicting site locations
|
||||
* update: add policy to update apps and platform separately
|
||||
* passkey: fix issue where passkeys were lost on restart
|
||||
* passkey: implement passwordless login
|
||||
* oidcserver: fix jwks_rsaonly response
|
||||
|
||||
[9.1.6]
|
||||
* apps: fix wrong disabled state for devices config
|
||||
* notifications: send email when manual platform and app update required
|
||||
* source install: support dockerfileName and build options
|
||||
* source install: persist buildConfig so restore, import, clone work correctly
|
||||
* search for matches in app links labels for apps view filter
|
||||
* restore: prune portBindings whose tcpPorts/udpPorts no longer exist
|
||||
* location: fix duplication of port bindings on submit
|
||||
* Update translations
|
||||
* location: show what DNS is being overwritten in location UI
|
||||
* backup site: remove the local disk provider
|
||||
* mail: update haraka to 3.1.4, tika to 3.3.0
|
||||
* solr: dynamically allocate java heap based on container mem
|
||||
|
||||
|
||||
@@ -4,15 +4,16 @@ import constants from './src/constants.js';
|
||||
import fs from 'node:fs';
|
||||
import ldapServer from './src/ldapserver.js';
|
||||
import net from 'node:net';
|
||||
import authServer from './src/authserver.js';
|
||||
import oidcServer from './src/oidcserver.js';
|
||||
import paths from './src/paths.js';
|
||||
import proxyAuth from './src/proxyauth.js';
|
||||
import safe from 'safetydance';
|
||||
import safe from '@cloudron/safetydance';
|
||||
import server from './src/server.js';
|
||||
import directoryServer from './src/directoryserver.js';
|
||||
import debugModule from 'debug';
|
||||
import logger from './src/logger.js';
|
||||
|
||||
const debug = debugModule('box:box');
|
||||
const { log } = logger('box');
|
||||
|
||||
let logFd;
|
||||
|
||||
@@ -59,39 +60,41 @@ const [error] = await safe(startServers());
|
||||
if (error) exitSync({ error, code: 1, message: 'Error starting servers' });
|
||||
|
||||
process.on('SIGHUP', async function () {
|
||||
debug('Received SIGHUP. Re-reading configs.');
|
||||
log('Received SIGHUP. Re-reading configs.');
|
||||
const conf = await directoryServer.getConfig();
|
||||
if (conf.enabled) await directoryServer.checkCertificate();
|
||||
});
|
||||
|
||||
process.on('SIGINT', async function () {
|
||||
debug('Received SIGINT. Shutting down.');
|
||||
log('Received SIGINT. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidcServer.stop();
|
||||
await authServer.stop();
|
||||
|
||||
setTimeout(() => {
|
||||
debug('Shutdown complete');
|
||||
log('Shutdown complete');
|
||||
process.exit();
|
||||
}, 2000); // need to wait for the task processes to die
|
||||
});
|
||||
|
||||
process.on('SIGTERM', async function () {
|
||||
debug('Received SIGTERM. Shutting down.');
|
||||
log('Received SIGTERM. Shutting down.');
|
||||
|
||||
await proxyAuth.stop();
|
||||
await server.stop();
|
||||
await directoryServer.stop();
|
||||
await ldapServer.stop();
|
||||
await oidcServer.stop();
|
||||
await authServer.stop();
|
||||
|
||||
setTimeout(() => {
|
||||
debug('Shutdown complete');
|
||||
log('Shutdown complete');
|
||||
process.exit();
|
||||
}, 2000); // need to wait for the task processes to die
|
||||
});
|
||||
|
||||
process.on('uncaughtException', (error) => exitSync({ error, code: 1, message: 'From uncaughtException handler.' }));
|
||||
process.on('uncaughtException', (uncaughtError) => exitSync({ error: uncaughtError, code: 1, message: 'From uncaughtException handler.' }));
|
||||
|
||||
+51
-11
@@ -2,19 +2,59 @@
|
||||
|
||||
<script>
|
||||
|
||||
const tmp = window.location.hash.slice(1).split('&');
|
||||
(async function () {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
const code = params.get('code');
|
||||
|
||||
// FIXME: implicit flow (response_type=code token) results in access_token query param. this is not secure
|
||||
tmp.forEach(function (pair) {
|
||||
if (pair.indexOf('access_token=') === 0) localStorage.token = pair.split('=')[1];
|
||||
});
|
||||
if (!code) {
|
||||
console.error('No authorization code in callback URL');
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
let redirectTo = '/';
|
||||
if (localStorage.getItem('redirectToHash')) {
|
||||
redirectTo += localStorage.getItem('redirectToHash');
|
||||
localStorage.removeItem('redirectToHash');
|
||||
}
|
||||
const codeVerifier = sessionStorage.getItem('pkce_code_verifier');
|
||||
const clientId = sessionStorage.getItem('pkce_client_id') || 'cid-webadmin';
|
||||
const apiOrigin = sessionStorage.getItem('pkce_api_origin') || '';
|
||||
|
||||
window.location.replace(redirectTo); // this removes us from history
|
||||
sessionStorage.removeItem('pkce_code_verifier');
|
||||
sessionStorage.removeItem('pkce_client_id');
|
||||
sessionStorage.removeItem('pkce_api_origin');
|
||||
|
||||
try {
|
||||
const response = await fetch(apiOrigin + '/openid/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: clientId,
|
||||
redirect_uri: window.location.origin + '/authcallback.html',
|
||||
code_verifier: codeVerifier
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok || !data.access_token) {
|
||||
console.error('Token exchange failed', data);
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.token = data.access_token;
|
||||
} catch (e) {
|
||||
console.error('Token exchange error', e);
|
||||
window.location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
let redirectTo = '/';
|
||||
if (localStorage.getItem('redirectToHash')) {
|
||||
redirectTo += localStorage.getItem('redirectToHash');
|
||||
localStorage.removeItem('redirectToHash');
|
||||
}
|
||||
|
||||
window.location.replace(redirectTo);
|
||||
})();
|
||||
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OpenID Confirm</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = <%- JSON.stringify({ name, clientName, userCode, form }) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/oidcdeviceconfirm.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OpenID Device Sign-in</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = <%- JSON.stringify({ name, message, form }) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/oidcdeviceinput.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>OpenID Device Success</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = <%- JSON.stringify({ name }) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/oidcdevicesuccess.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -4,13 +4,7 @@
|
||||
<title><%= name %> OpenID Error</title>
|
||||
|
||||
<script>
|
||||
window.cloudron = <%- JSON.stringify({
|
||||
iconUrl: iconUrl,
|
||||
name: name,
|
||||
errorMessage: errorMessage,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
window.cloudron = <%- JSON.stringify({ iconUrl, name, errorMessage, footer, language }) %>;
|
||||
</script>
|
||||
|
||||
</head>
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
name: name,
|
||||
note: note,
|
||||
submitUrl: submitUrl,
|
||||
passkeyAuthOptionsUrl: passkeyAuthOptionsUrl,
|
||||
passkeyLoginUrl: passkeyLoginUrl,
|
||||
footer: footer,
|
||||
language: language
|
||||
}) %>;
|
||||
|
||||
Generated
+1162
-3128
File diff suppressed because it is too large
Load Diff
+12
-12
@@ -7,11 +7,11 @@
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@simplewebauthn/browser": "^13.2.2",
|
||||
"@cloudron/pankow": "^4.1.3",
|
||||
"@simplewebauthn/browser": "^13.3.0",
|
||||
"@cloudron/pankow": "^4.1.8",
|
||||
"@fontsource/inter": "^5.2.8",
|
||||
"@fortawesome/fontawesome-free": "^7.2.0",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@vitejs/plugin-vue": "^6.0.5",
|
||||
"@xterm/addon-attach": "^0.12.0",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
@@ -19,15 +19,15 @@
|
||||
"async": "^3.2.6",
|
||||
"chart.js": "^4.5.1",
|
||||
"chartjs-plugin-annotation": "^3.1.0",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-vue": "^10.7.0",
|
||||
"marked": "^17.0.3",
|
||||
"eslint": "^10.1.0",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"marked": "^17.0.5",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.6.0",
|
||||
"vite": "^7.3.1",
|
||||
"vite-plugin-singlefile": "^2.3.0",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^5.0.3"
|
||||
"moment-timezone": "^0.6.1",
|
||||
"vite": "^8.0.3",
|
||||
"vite-plugin-singlefile": "^2.3.2",
|
||||
"vue": "^3.5.31",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^5.0.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
"configure": "Nakonfigurovat",
|
||||
"restart": "Restartovat",
|
||||
"reset": "Zresetovat",
|
||||
"loadMore": "Načíst více"
|
||||
"loadMore": "Načíst více",
|
||||
"setup": "Nastavit",
|
||||
"disable": "Zakázat"
|
||||
},
|
||||
"rebootDialog": {
|
||||
"title": "Restart serveru",
|
||||
@@ -348,8 +350,6 @@
|
||||
"allowedIpRanges": "Povolený rozsah IP adres"
|
||||
},
|
||||
"changePasswordAction": "Změnit heslo",
|
||||
"disable2FAAction": "Zakázat 2FA",
|
||||
"enable2FAAction": "Povolit 2FA",
|
||||
"passwordResetNotification": {
|
||||
"body": "E-mail odeslán na {{ email }}"
|
||||
},
|
||||
@@ -363,9 +363,23 @@
|
||||
},
|
||||
"twoFactorAuth": {
|
||||
"title": "Dvoufaktorová autentizace",
|
||||
"disabled": "Zakázáno",
|
||||
"totpEnabled": "Použít časově omezené jednorázové heslo (TOTP)",
|
||||
"passkeyEnabled": "Použít passkey"
|
||||
"totpEnabled": "Povoleno",
|
||||
"passkeyEnabled": "Povoleno",
|
||||
"totpTitle": "TOTP",
|
||||
"passkeyTitle": "Passkey"
|
||||
},
|
||||
"notSet": "nenastaveno",
|
||||
"enablePasskey": {
|
||||
"title": "Povolit passkey"
|
||||
},
|
||||
"enableTotp": {
|
||||
"title": "Povolit TOTP"
|
||||
},
|
||||
"disableTotp": {
|
||||
"title": "Zakázat TOTP"
|
||||
},
|
||||
"disablePasskey": {
|
||||
"title": "Zakázat passkey"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -650,18 +664,17 @@
|
||||
"updateAvailableAction": "Aktualizace k dispozici",
|
||||
"stopUpdateAction": "Zastavit aktualizaci",
|
||||
"disabled": "Zakázáno",
|
||||
"schedule": "Naplánovat aktualizace",
|
||||
"onLatest": "poslední",
|
||||
"description": "Aktualizace platformy a aplikací se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>."
|
||||
"description": "Aktualizace se aplikují v nastavený čas podle <a href=\"/#/system-settings\">časové zóny systému</a>.",
|
||||
"config": "Automatické aktualizace",
|
||||
"platformAndApps": "Platforma a aplikace",
|
||||
"appsOnly": "Pouze aplikace"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"disableCheckbox": "Zakázat automatické aktualizace",
|
||||
"enableCheckbox": "Povolit automatické aktualizace",
|
||||
"selectOne": "Vyberte alespoň jeden den a čas",
|
||||
"days": "Dny",
|
||||
"hours": "Hodiny",
|
||||
"description": "Nastavte dny a časy pro automatické aktualizace platformy a aplikací. Ujistěte se, že se tento plán nepřekrývá s plány zálohování.",
|
||||
"title": "Konfigurovat čas automatických aktualizací"
|
||||
"description": "Nastavte dny a časy pro automatické aktualizace platformy a aplikací. Ujistěte se, že se tento plán nepřekrývá s plány zálohování."
|
||||
},
|
||||
"updateDialog": {
|
||||
"title": "Aktualizovat Cloudron",
|
||||
@@ -680,6 +693,14 @@
|
||||
"registryConfig": {
|
||||
"provider": "Poskytovatel docker registrů",
|
||||
"providerOther": "Jiné"
|
||||
},
|
||||
"configureUpdates": {
|
||||
"title": "Konfigurovat automatické aktualizace",
|
||||
"policy": "Politika",
|
||||
"policyDescription": "Vyberte, co se bude automaticky aktualizovat",
|
||||
"days": "Dnů/y",
|
||||
"hours": "Hodin/y",
|
||||
"schedule": "Plány záloh"
|
||||
}
|
||||
},
|
||||
"branding": {
|
||||
@@ -890,7 +911,9 @@
|
||||
"appDown": "Aplikace je mimo provoz",
|
||||
"rebootRequired": "Vyžadován restart serveru",
|
||||
"cloudronUpdateFailed": "Aktualizace Cloudronu selhala",
|
||||
"diskSpace": "Nedostatek místa na disku"
|
||||
"diskSpace": "Nedostatek místa na disku",
|
||||
"appAutoUpdateFailed": "Automatická aktualizace aplikace selhala",
|
||||
"manualUpdateRequired": "Platforma nebo aplikace vyžaduje ruční aktualizaci"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Na váš primární e-mail bude odeslán e-mail souhrn těchto vybraných událostí."
|
||||
@@ -947,7 +970,6 @@
|
||||
"noRedirections": "Žádné přesměrované domény",
|
||||
"addRedirectionAction": "Přidat přesměrování",
|
||||
"saveAction": "Uložit",
|
||||
"dnsoverwrite": "Některé DNS záznamy již existují. Potvrďte přepsání.",
|
||||
"aliases": "Aliasy",
|
||||
"addAliasAction": "Přidat alias",
|
||||
"noAliases": "Žádné aliasy pro domény"
|
||||
@@ -1600,7 +1622,9 @@
|
||||
"errorIncorrect2FAToken": "Token 2FA je neplatný",
|
||||
"errorInternal": "Interní chyba, zkuste akci opakovat později",
|
||||
"loginAction": "Přihlásit se",
|
||||
"usePasskeyAction": "Použít passkey"
|
||||
"usePasskeyAction": "Použít passkey",
|
||||
"passkeyAction": "Přihlásit se přes passkey",
|
||||
"errorPasskeyFailed": "Přihlášení pomocí passkey selhalo"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Reset hesla",
|
||||
|
||||
@@ -296,8 +296,6 @@
|
||||
"password": "Adgangskode til bekræftelse"
|
||||
},
|
||||
"changePasswordAction": "Ændre adgangskode",
|
||||
"disable2FAAction": "Deaktivere 2FA",
|
||||
"enable2FAAction": "Aktiver 2FA",
|
||||
"passwordResetNotification": {
|
||||
"body": "E-mail sendt til {{ email }}"
|
||||
}
|
||||
@@ -555,11 +553,8 @@
|
||||
"updateScheduleDialog": {
|
||||
"selectOne": "Vælg mindst én dag og ét tidspunkt",
|
||||
"description": "Vælg de dage og timer, hvor Cloudron vil anvende automatiske platforms- og appopdateringer. Pas på, at denne tidsplan ikke overlapper med <a href=\"/#/backups\">backup-tidsplanen</a>.",
|
||||
"title": "Konfigurer tidsplan for automatisk opdatering",
|
||||
"disableCheckbox": "Deaktivere automatiske opdateringer",
|
||||
"enableCheckbox": "Aktivere automatiske opdateringer",
|
||||
"days": "Dage",
|
||||
"hours": "Timer"
|
||||
"enableCheckbox": "Aktivere automatiske opdateringer"
|
||||
},
|
||||
"updateDialog": {
|
||||
"unstableWarning": "Denne opdatering er en præudgave og betragtes ikke som stabil endnu. Opdatering sker på egen risiko.",
|
||||
|
||||
@@ -134,7 +134,6 @@
|
||||
"updateAvailableAction": "Aktualisierung verfügbar",
|
||||
"description": "Plattform und App-Aktualisierungen werden automatisch, basierend auf dem Zeitplan in der <a href=\"/#/system-settings\">Systemzeitzone</a> ausgeführt.",
|
||||
"disabled": "Deaktiviert",
|
||||
"schedule": "Aktualisierungszeitplan",
|
||||
"onLatest": "neueste"
|
||||
},
|
||||
"appstoreAccount": {
|
||||
@@ -154,13 +153,10 @@
|
||||
}
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"hours": "Stunden",
|
||||
"disableCheckbox": "Automatische Aktualisierung deaktivieren",
|
||||
"enableCheckbox": "Automatische Aktualisierung aktivieren",
|
||||
"selectOne": "Mindestens einen Tag und eine Uhrzeit wählen",
|
||||
"days": "Tage",
|
||||
"description": "Tage und Stunden auswählen, an denen Cloudron das System und die Anwendungen aktualisieren soll. Der Zeitplan soll sich nicht mit dem Zeitplan für Datensicherungen überschneiden.",
|
||||
"title": "Automatische Aktualisierung konfigurieren"
|
||||
"description": "Tage und Stunden auswählen, an denen Cloudron das System und die Anwendungen aktualisieren soll. Der Zeitplan soll sich nicht mit dem Zeitplan für Datensicherungen überschneiden."
|
||||
},
|
||||
"timezone": {
|
||||
"description": "Dient dazu, Datensicherungen und Updates zu planen. UI-Zeitstempel folgen immer der Zeitzone des Browsers.",
|
||||
@@ -370,8 +366,6 @@
|
||||
"description": "App-Passwörter sind eine Sicherheitsmaßnahme zum Schutz des Cloudron-User-Kontos. Sobald eingerichtet, kann die Anmeldung (zusätzlich) mit dem Usernamen und dem hier angezeigtem Passwort erfolgen. Hinweis: sinnvoll bei nicht vertrauenswürdigen mobilen Anwendungen oder Desktop-Clients.",
|
||||
"title": "App-Passwörter"
|
||||
},
|
||||
"enable2FAAction": "2FA aktivieren",
|
||||
"disable2FAAction": "2FA deaktivieren",
|
||||
"changePasswordAction": "Passwort ändern",
|
||||
"createApiToken": {
|
||||
"copyNow": "API-Token kopieren. Hinweis: keine erneute Anzeige des API-Tokens.",
|
||||
@@ -1420,8 +1414,7 @@
|
||||
"addRedirectionAction": "Eine Weiterleitung hinzufügen",
|
||||
"noAliases": "Keine Aliasse",
|
||||
"addAliasAction": "Alias hinzufügen",
|
||||
"aliases": "Aliasse",
|
||||
"dnsoverwrite": "Einige DNS-Einträge existieren bereits. Mit dem Überschreiben einverstanden."
|
||||
"aliases": "Aliasse"
|
||||
},
|
||||
"updateDialog": {
|
||||
"subscriptionExpired": "Das Cloudron-Abonnement ist abgelaufen. Bitte ein Abonnement einrichten, um die Anwendung zu aktualisieren.",
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
"configure": "Configure",
|
||||
"restart": "Restart",
|
||||
"reset": "Reset",
|
||||
"loadMore": "Load more"
|
||||
"loadMore": "Load more",
|
||||
"setup": "Set up",
|
||||
"disable": "Disable"
|
||||
},
|
||||
"rebootDialog": {
|
||||
"title": "Reboot Server",
|
||||
@@ -348,8 +350,6 @@
|
||||
"allowedIpRanges": "Allowed IP range(s)"
|
||||
},
|
||||
"changePasswordAction": "Change password",
|
||||
"disable2FAAction": "Disable 2FA",
|
||||
"enable2FAAction": "Enable 2FA",
|
||||
"passwordResetNotification": {
|
||||
"body": "Email sent to {{ email }}"
|
||||
},
|
||||
@@ -363,9 +363,23 @@
|
||||
},
|
||||
"twoFactorAuth": {
|
||||
"title": "Two-factor authentication",
|
||||
"disabled": "Disabled",
|
||||
"totpEnabled": "Using time-based one-time password (TOTP)",
|
||||
"passkeyEnabled": "Using passkey"
|
||||
"totpEnabled": "Enabled",
|
||||
"passkeyEnabled": "Enabled",
|
||||
"totpTitle": "TOTP",
|
||||
"passkeyTitle": "Passkey"
|
||||
},
|
||||
"notSet": "Not set",
|
||||
"enablePasskey": {
|
||||
"title": "Enable passkey"
|
||||
},
|
||||
"enableTotp": {
|
||||
"title": "Enable TOTP"
|
||||
},
|
||||
"disableTotp": {
|
||||
"title": "Disable TOTP"
|
||||
},
|
||||
"disablePasskey": {
|
||||
"title": "Disable Passkey"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -707,17 +721,16 @@
|
||||
"updateAvailableAction": "Update available",
|
||||
"stopUpdateAction": "Stop update",
|
||||
"disabled": "Disabled",
|
||||
"schedule": "Update schedule",
|
||||
"description": "Platform and app updates are applied on the configured schedule, using the <a href=\"/#/system-settings\">System time zone</a>.",
|
||||
"onLatest": "latest"
|
||||
"description": "Updates are applied on the configured schedule, using the <a href=\"/#/system-settings\">System time zone</a>.",
|
||||
"onLatest": "latest",
|
||||
"config": "Automatic updates",
|
||||
"appsOnly": "Apps only",
|
||||
"platformAndApps": "Platform & apps"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"title": "Configure Automatic Update Schedule",
|
||||
"disableCheckbox": "Disable automatic updates",
|
||||
"enableCheckbox": "Enable automatic updates",
|
||||
"selectOne": "Select at least one day and time",
|
||||
"days": "Days",
|
||||
"hours": "Hours",
|
||||
"description": "Set the days and times for automatic platform and app updates. Ensure this schedule doesn’t overlap with backup schedules."
|
||||
},
|
||||
"updateDialog": {
|
||||
@@ -737,6 +750,14 @@
|
||||
"registryConfig": {
|
||||
"provider": "Docker registry provider",
|
||||
"providerOther": "Other"
|
||||
},
|
||||
"configureUpdates": {
|
||||
"title": "Configure Automatic Updates",
|
||||
"policy": "Policy",
|
||||
"policyDescription": "Choose what gets updated automatically",
|
||||
"days": "Days",
|
||||
"hours": "Hours",
|
||||
"schedule": "Schedule"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -890,7 +911,9 @@
|
||||
"appDown": "App is down",
|
||||
"rebootRequired": "Server reboot required",
|
||||
"cloudronUpdateFailed": "Cloudron update failed",
|
||||
"diskSpace": "Low disk space"
|
||||
"diskSpace": "Low disk space",
|
||||
"appAutoUpdateFailed": "App automatic update failed",
|
||||
"manualUpdateRequired": "Platform or app requires manual update"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "An email will be sent for the selected events to your primary email."
|
||||
@@ -1207,7 +1230,7 @@
|
||||
"aliases": "Aliases",
|
||||
"addAliasAction": "Add an alias",
|
||||
"noAliases": "No alias domains",
|
||||
"dnsoverwrite": "Some DNS records already exist. Agree to overwrite."
|
||||
"overwriteDns": "Overwrite existing DNS records of {domains}"
|
||||
},
|
||||
"accessControl": {
|
||||
"userManagement": {
|
||||
@@ -1520,7 +1543,9 @@
|
||||
"errorIncorrect2FAToken": "2FA token is invalid",
|
||||
"errorInternal": "Internal error, try again later",
|
||||
"loginAction": "Log in",
|
||||
"usePasskeyAction": "Use passkey"
|
||||
"usePasskeyAction": "Use passkey",
|
||||
"errorPasskeyFailed": "Failed to login with passkey",
|
||||
"passkeyAction": "Log in with a passkey"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Password reset",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -263,8 +263,6 @@
|
||||
"title": "Jetons de connexion",
|
||||
"description": "Vous avez {{ webadminTokens.length }} jeton(s) web actif(s) et {{ cliTokens.length }} jeton(s) CLI."
|
||||
},
|
||||
"disable2FAAction": "Désactiver l'authentification à deux facteurs (2FA)",
|
||||
"enable2FAAction": "Activer l'authentification à deux facteurs (2FA)",
|
||||
"passwordResetNotification": {
|
||||
"body": "Email envoyé à {{ email }}"
|
||||
}
|
||||
@@ -517,12 +515,9 @@
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"description": "Sélectionnez les jours et heures de lancement des mises à jour de la plateforme et des applications. Veillez à ne pas planifier les mises à jour au même moment que la <a href=\"/#/backups\">sauvegarde</a>.",
|
||||
"hours": "Heures",
|
||||
"days": "Jours",
|
||||
"selectOne": "Sélectionnez au moins un jour et une heure",
|
||||
"enableCheckbox": "Activer les mises à jour automatiques",
|
||||
"disableCheckbox": "Désactiver les mises à jour automatiques",
|
||||
"title": "Planification des mises à jour automatiques"
|
||||
"disableCheckbox": "Désactiver les mises à jour automatiques"
|
||||
},
|
||||
"updates": {
|
||||
"stopUpdateAction": "Interrompre la mise à jour",
|
||||
|
||||
@@ -44,7 +44,9 @@
|
||||
"restart": "Mulai ulang",
|
||||
"reset": "Atur Ulang",
|
||||
"logs": "Log",
|
||||
"loadMore": "Muat lebih banyak"
|
||||
"loadMore": "Muat lebih banyak",
|
||||
"setup": "Siapkan",
|
||||
"disable": "Nonaktifkan"
|
||||
},
|
||||
"searchPlaceholder": "Cari",
|
||||
"actions": "Tindakan",
|
||||
@@ -89,7 +91,7 @@
|
||||
"userManagementAllUsers": "Izinkan semua pengguna di Cloudron ini",
|
||||
"userManagementSelectUsers": "Hanya izinkan pengguna dan grup berikut ini",
|
||||
"errorUserManagementSelectAtLeastOne": "Pilih setidaknya satu pengguna atau grup",
|
||||
"configuredForCloudronEmail": "Aplikasi ini telah dikonfigurasi sebelumnya untuk digunakan dengan <a href=\"{{ emailDocsLink }}\" target=\"_blank\">E-mail Cloudron </a>.",
|
||||
"configuredForCloudronEmail": "Aplikasi ini telah dikonfigurasi sebelumnya untuk digunakan dengan <a href=\"{{ emailDocsLink }}\" target=\"_blank\">E-mail Cloudron</a>.",
|
||||
"cloudflarePortWarning": "Proksi Cloudflare harus dinonaktifkan agar domain aplikasi dapat mengakses port ini",
|
||||
"portReadOnly": "hanya baca",
|
||||
"ephemeralPortWarning": "Menggunakan port dinamis dapat menyebabkan konflik yang tidak terduga."
|
||||
@@ -326,8 +328,6 @@
|
||||
"copyNow": "Silakan salin kata sandi sekarang. Kata sandi ini tidak akan ditampilkan lagi untuk alasan keamanan.",
|
||||
"expiresAt": "Tanggal kedaluwarsa"
|
||||
},
|
||||
"disable2FAAction": "Nonaktifkan 2FA",
|
||||
"enable2FAAction": "Aktifkan 2FA",
|
||||
"title": "Profil",
|
||||
"primaryEmail": "E-mail utama",
|
||||
"passwordRecoveryEmail": "E-mail pemulihan kata sandi",
|
||||
@@ -363,9 +363,23 @@
|
||||
},
|
||||
"twoFactorAuth": {
|
||||
"title": "Autentikasi dua faktor",
|
||||
"disabled": "Dinonaktifkan",
|
||||
"totpEnabled": "Menggunakan kata sandi sekali pakai berbasis waktu (TOTP)",
|
||||
"passkeyEnabled": "Menggunakan passkey"
|
||||
"totpEnabled": "Diaktifkan",
|
||||
"passkeyEnabled": "Diaktifkan",
|
||||
"totpTitle": "TOTP",
|
||||
"passkeyTitle": "Passkey"
|
||||
},
|
||||
"notSet": "Belum diatur",
|
||||
"enablePasskey": {
|
||||
"title": "Aktifkan passkey"
|
||||
},
|
||||
"enableTotp": {
|
||||
"title": "Aktifkan TOTP"
|
||||
},
|
||||
"disableTotp": {
|
||||
"title": "Nonaktifkan TOTP"
|
||||
},
|
||||
"disablePasskey": {
|
||||
"title": "Nonaktifkan Passkey"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -727,17 +741,16 @@
|
||||
"updateAvailableAction": "Pembaruan tersedia",
|
||||
"stopUpdateAction": "Hentikan pembaruan",
|
||||
"disabled": "Dinonaktifkan",
|
||||
"schedule": "Jadwal pembaruan",
|
||||
"description": "Pembaruan platform dan aplikasi diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
|
||||
"onLatest": "terbaru"
|
||||
"description": "Pembaruan diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan <a href=\"/#/system-settings\">Zona waktu sistem</a>.",
|
||||
"onLatest": "terbaru",
|
||||
"config": "Pembaruan otomatis",
|
||||
"appsOnly": "Hanya aplikasi",
|
||||
"platformAndApps": "Platform & aplikasi"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"title": "Konfigurasi Jadwal Pembaruan Otomatis",
|
||||
"disableCheckbox": "Nonaktifkan pembaruan otomatis",
|
||||
"enableCheckbox": "Aktifkan pembaruan otomatis",
|
||||
"selectOne": "Pilih setidaknya satu hari dan satu waktu",
|
||||
"days": "Hari",
|
||||
"hours": "Jam",
|
||||
"description": "Atur hari dan waktu untuk pembaruan otomatis platform dan aplikasi. Pastikan jadwal ini tidak tumpang tindih dengan jadwal pencadangan."
|
||||
},
|
||||
"updateDialog": {
|
||||
@@ -757,6 +770,14 @@
|
||||
"registryConfig": {
|
||||
"provider": "Penyedia registri Docker",
|
||||
"providerOther": "Lainnya"
|
||||
},
|
||||
"configureUpdates": {
|
||||
"title": "Konfigurasi Pembaruan Otomatis",
|
||||
"policy": "Kebijakan",
|
||||
"policyDescription": "Pilih apa yang diperbarui secara otomatis",
|
||||
"days": "Hari",
|
||||
"hours": "Jam",
|
||||
"schedule": "Jadwal"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -1186,8 +1207,7 @@
|
||||
"saveAction": "Simpan",
|
||||
"aliases": "Alias",
|
||||
"addAliasAction": "Tambahkan alias",
|
||||
"noAliases": "Tidak ada domain alias",
|
||||
"dnsoverwrite": "Beberapa catatan DNS sudah ada. Setuju untuk menimpa."
|
||||
"noAliases": "Tidak ada domain alias"
|
||||
},
|
||||
"accessControl": {
|
||||
"userManagement": {
|
||||
@@ -1403,7 +1423,7 @@
|
||||
"title": "Pembaruan otomatis"
|
||||
},
|
||||
"updates": {
|
||||
"description": "Cloudron secara otomatis memeriksa pembaruan di App Store. Anda juga dapat memeriksanya secara manual."
|
||||
"description": "Cloudron secara otomatis memeriksa pembaruan. Anda juga dapat memeriksanya secara manual."
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -1636,7 +1656,8 @@
|
||||
"title": "Situs Cadangan",
|
||||
"emptyPlaceholder": "Tidak ada situs cadangan",
|
||||
"lastRun": "Terakhir dijalankan",
|
||||
"description": "Lokasi cadangan menunjukkan di mana cadangan sistem dan cadangan aplikasi disimpan. Cadangan aplikasi dapat dipulihkan secara terpisah."
|
||||
"description": "Lokasi cadangan menunjukkan di mana cadangan sistem dan cadangan aplikasi disimpan. Cadangan aplikasi dapat dipulihkan secara terpisah.",
|
||||
"noAutomaticUpdateBackupWarning": "Tidak ada situs cadangan yang dikonfigurasi untuk menyimpan cadangan pembaruan otomatis. Aktifkan \"Simpan cadangan pembaruan otomatis di sini\" pada setidaknya satu situs cadangan untuk memungkinkan pembaruan otomatis."
|
||||
},
|
||||
"site": {
|
||||
"removeDialog": {
|
||||
@@ -1668,7 +1689,9 @@
|
||||
"appDown": "Aplikasi sedang tidak berfungsi",
|
||||
"rebootRequired": "Diperlukan menyalakan ulang server",
|
||||
"cloudronUpdateFailed": "Pembaruan Cloudron gagal",
|
||||
"diskSpace": "Ruang disk hampir penuh"
|
||||
"diskSpace": "Ruang disk hampir penuh",
|
||||
"appAutoUpdateFailed": "Pembaruan otomatis aplikasi gagal",
|
||||
"manualUpdateRequired": "Platform atau aplikasi memerlukan pembaruan manual"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "E-mail akan dikirimkan ke e-mail utama Anda untuk acara-acara yang dipilih."
|
||||
@@ -1694,7 +1717,9 @@
|
||||
"errorIncorrect2FAToken": "Token 2FA tidak valid",
|
||||
"errorInternal": "Terjadi kesalahan internal, coba lagi nanti",
|
||||
"loginAction": "Masuk",
|
||||
"usePasskeyAction": "Gunakan passkey"
|
||||
"usePasskeyAction": "Gunakan passkey",
|
||||
"errorPasskeyFailed": "Gagal masuk dengan passkey",
|
||||
"passkeyAction": "Masuk dengan passkey"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Pengaturan ulang kata sandi",
|
||||
|
||||
@@ -560,8 +560,6 @@
|
||||
"title": "Backup"
|
||||
},
|
||||
"profile": {
|
||||
"enable2FAAction": "Abilita 2FA",
|
||||
"disable2FAAction": "Disabilita 2FA",
|
||||
"changePasswordAction": "Cambia Password",
|
||||
"createApiToken": {
|
||||
"copyNow": "Copia il token API ora. Non verrà mostrato di nuovo per motivi di sicurezza.",
|
||||
@@ -782,12 +780,9 @@
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"description": "Seleziona i giorni e gli orari durante i quali Cloudron applicherà gli aggiornamenti automatici della piattaforma e dell'app. Fai attenzione a non sovrapporre questa pianificazione alla <a href=\"/#/backups\">pianificazione dei backup</a>.",
|
||||
"hours": "Ore",
|
||||
"days": "Giorni",
|
||||
"selectOne": "Seleziona almeno un giorno e un'ora",
|
||||
"enableCheckbox": "Abilita Aggiornamenti Automatici",
|
||||
"disableCheckbox": "Disabilita Aggiornamenti Automatici",
|
||||
"title": "Configura pianificazione aggiornamenti automatici"
|
||||
"disableCheckbox": "Disabilita Aggiornamenti Automatici"
|
||||
},
|
||||
"updates": {
|
||||
"stopUpdateAction": "Ferma Aggiornamento",
|
||||
|
||||
@@ -47,7 +47,9 @@
|
||||
"configure": "Configureer",
|
||||
"restart": "Herstart",
|
||||
"reset": "Reset",
|
||||
"loadMore": "Laad meer"
|
||||
"loadMore": "Laad meer",
|
||||
"setup": "Instellen",
|
||||
"disable": "Uitschakelen"
|
||||
},
|
||||
"rebootDialog": {
|
||||
"title": "Herstart Server",
|
||||
@@ -348,8 +350,6 @@
|
||||
"allowedIpRanges": "Toegestane IP range(s)"
|
||||
},
|
||||
"changePasswordAction": "Verander Wachtwoord",
|
||||
"disable2FAAction": "Twee-Factor (2FA) authenticatie uitschakelen",
|
||||
"enable2FAAction": "Twee-Factor (2FA) authenticatie inschakelen",
|
||||
"passwordResetNotification": {
|
||||
"body": "E-mail gestuurd naar {{ email }}"
|
||||
},
|
||||
@@ -363,9 +363,23 @@
|
||||
},
|
||||
"twoFactorAuth": {
|
||||
"title": "Twee-Factor (2FA) authenticatie",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"totpEnabled": "Gebruikt tijdgebaseerd eenmalige wachtwoord (TOTP)",
|
||||
"passkeyEnabled": "Gebruikt passkey"
|
||||
"totpEnabled": "Ingeschakeld",
|
||||
"passkeyEnabled": "Ingeschakeld",
|
||||
"totpTitle": "TOTP",
|
||||
"passkeyTitle": "Passkey"
|
||||
},
|
||||
"notSet": "Niet ingesteld",
|
||||
"enablePasskey": {
|
||||
"title": "Passkey activeren"
|
||||
},
|
||||
"enableTotp": {
|
||||
"title": "TOTP activeren"
|
||||
},
|
||||
"disableTotp": {
|
||||
"title": "TOTP Uitschakelen"
|
||||
},
|
||||
"disablePasskey": {
|
||||
"title": "Passkey uitschakelen"
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -771,8 +785,7 @@
|
||||
"noRedirections": "Geen domein-omleidingen",
|
||||
"noAliases": "Geen alias-domeinen",
|
||||
"addAliasAction": "Alias toevoegen",
|
||||
"aliases": "Aliassen",
|
||||
"dnsoverwrite": "Sommige DNS records bestaan al. Weet je zeker dat ze overschreven moeten worden?"
|
||||
"aliases": "Aliassen"
|
||||
},
|
||||
"accessControl": {
|
||||
"userManagement": {
|
||||
@@ -1144,18 +1157,17 @@
|
||||
"checkForUpdatesAction": "Controleer op updates",
|
||||
"updateAvailableAction": "Update beschikbaar",
|
||||
"stopUpdateAction": "Stop update",
|
||||
"description": "Platform en app updates worden toegepast met de geconfigureerde planning met deze <a href=\"/#/system-locale\">Systeem tijdzone</a>.",
|
||||
"description": "Updates worden toegepast volgens het geconfigureerde schema, met behulp van de <a href=\"/#/system-settings\">System time zone</a>.",
|
||||
"disabled": "Uitgeschakeld",
|
||||
"schedule": "Update planning",
|
||||
"onLatest": "Laatste"
|
||||
"onLatest": "Laatste",
|
||||
"config": "Automatische updates",
|
||||
"appsOnly": "Alleen Apps",
|
||||
"platformAndApps": "Platform & Apps"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"disableCheckbox": "Automatische updates uitschakelen",
|
||||
"enableCheckbox": "Automatische updates inschakelen",
|
||||
"selectOne": "Selecteer minstens één dag en tijd",
|
||||
"days": "Dagen",
|
||||
"hours": "Uren",
|
||||
"title": "Automatische Update Planning configureren",
|
||||
"description": "Stel de dagen en uren in voor automatische updates van het platform en apps. Zorg ervoor dat dit schema niet overlapt met de back-upschema's."
|
||||
},
|
||||
"updateDialog": {
|
||||
@@ -1176,6 +1188,14 @@
|
||||
"registryConfig": {
|
||||
"provider": "Docker registry aanbieder",
|
||||
"providerOther": "Anders"
|
||||
},
|
||||
"configureUpdates": {
|
||||
"title": "Automatische updates configureren",
|
||||
"policy": "Beleid",
|
||||
"policyDescription": "Kies wat er automatisch wordt bijgewerkt",
|
||||
"days": "Dagen",
|
||||
"hours": "Uren",
|
||||
"schedule": "Planning"
|
||||
}
|
||||
},
|
||||
"support": {
|
||||
@@ -1233,7 +1253,9 @@
|
||||
"appDown": "App werkt niet",
|
||||
"rebootRequired": "Server herstart noodzakelijk",
|
||||
"cloudronUpdateFailed": "Cloudron update mislukt",
|
||||
"diskSpace": "Weinig diskruimte"
|
||||
"diskSpace": "Weinig diskruimte",
|
||||
"appAutoUpdateFailed": "Automatische update van de app is mislukt",
|
||||
"manualUpdateRequired": "Platform of app moet handmatig geüpdatet worden"
|
||||
},
|
||||
"settingsDialog": {
|
||||
"description": "Een e-mail wordt verstuurd voor de geselecteerde gebeurtenissen naar je primaire e-mail."
|
||||
@@ -1520,7 +1542,9 @@
|
||||
"errorIncorrect2FAToken": "2FA token is niet geldig",
|
||||
"errorInternal": "Interne fout, probeer later opnieuw",
|
||||
"loginAction": "Inloggen",
|
||||
"usePasskeyAction": "Gebruik een passkey"
|
||||
"usePasskeyAction": "Gebruik een passkey",
|
||||
"errorPasskeyFailed": "Inloggen met passkey mislukt",
|
||||
"passkeyAction": "Inloggen met een passkey"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Wachtwoord herstellen",
|
||||
|
||||
@@ -180,8 +180,6 @@
|
||||
"description": "As palavras-passe da aplicação são uma medida de segurança para proteger a sua conta de utilizador Cloudron. Se precisar de aceder a uma aplicação Cloudron a partir de uma aplicação móvel ou cliente não fidedigno, pode iniciar a sessão com o seu nome de utilizador e a palavra-passe alternativa gerada aqui."
|
||||
},
|
||||
"changePasswordAction": "Alterar palavra-passe",
|
||||
"disable2FAAction": "Desativar 2FA",
|
||||
"enable2FAAction": "Ativar 2FA",
|
||||
"removeAppPassword": {
|
||||
"title": "Remover Palavra-passe da Aplicação",
|
||||
"description": "Remover a palavra-passe da aplicação \"{{ name }}\"?"
|
||||
@@ -619,7 +617,6 @@
|
||||
},
|
||||
"updates": {
|
||||
"checkForUpdatesAction": "Procurar por Atualizações",
|
||||
"schedule": "Agendar",
|
||||
"updateAvailableAction": "Disponível Atualização",
|
||||
"stopUpdateAction": "Parar Atualização",
|
||||
"disabled": "Desativada"
|
||||
@@ -633,8 +630,6 @@
|
||||
"blockingAppsInfo": "Por favor, aguarde que as operações em cima terminem."
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"days": "Dias",
|
||||
"hours": "Horas",
|
||||
"disableCheckbox": "Desativar Atualizações Automáticas",
|
||||
"enableCheckbox": "Ativar Atualizações Automáticas",
|
||||
"selectOne": "Selecione pelo menos um dia e hora"
|
||||
|
||||
@@ -283,18 +283,23 @@
|
||||
"disable": "Отключить"
|
||||
},
|
||||
"enable2FA": {
|
||||
"authenticatorAppDescription": "Используйте 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>) или аналогичные TOTP приложения для сканирования секретного кода.",
|
||||
"authenticatorAppDescription": "Используйте 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>) или аналогичные TOTP приложения для сканирования секретного кода.",
|
||||
"title": "Включить двухфакторную аутентификацию (2FA)",
|
||||
"token": "Токен",
|
||||
"enable": "Включить",
|
||||
"mandatorySetup": "Для доступа к панели управления требуется 2FA. Пожалуйста, закончите настройку, чтобы продолжить."
|
||||
"mandatorySetup": "Для доступа к панели управления требуется 2FA. Пожалуйста, закончите настройку, чтобы продолжить.",
|
||||
"passkeyOption": "Ключ доступа",
|
||||
"totpOption": "TOTP",
|
||||
"registerPasskey": "Настроить ключ доступа",
|
||||
"passkeyDescription": "Браузер предложит вам создать ключ доступа с помощью биометрических данных вашего устройства или менеджера паролей."
|
||||
},
|
||||
"appPasswords": {
|
||||
"description": "Пароли приложений - это мера безопасности, направленная на защиту вашего аккаунта Cloudron от несанкционированного доступа. Если вам необходим доступ к Cloudron с ненадёжного мобильного или десктопного приложения, вы можете войти под своим именем пользователя и использовать с ним специально сгенерированный пароль.",
|
||||
"title": "Пароли приложений",
|
||||
"app": "Приложение",
|
||||
"name": "Имя",
|
||||
"noPasswordsPlaceholder": "Пароли приложений отсутствуют"
|
||||
"noPasswordsPlaceholder": "Пароли приложений отсутствуют",
|
||||
"expires": "Истекает"
|
||||
},
|
||||
"title": "Профиль",
|
||||
"primaryEmail": "Основной Email",
|
||||
@@ -331,7 +336,8 @@
|
||||
"name": "Имя пароля",
|
||||
"app": "Приложение",
|
||||
"description": "Используйте этот пароль для аутентификации в приложении:",
|
||||
"copyNow": "Пожалуйста, скопируйте сгенерированный пароль. Он не будет показан снова из соображений безопасности."
|
||||
"copyNow": "Пожалуйста, скопируйте сгенерированный пароль. Он не будет показан снова из соображений безопасности.",
|
||||
"expiresAt": "Истекает в"
|
||||
},
|
||||
"createApiToken": {
|
||||
"copyNow": "Пожалуйста, скопируйте сгенерированный API Токен. Он не будет показан снова из соображений безопасности.",
|
||||
@@ -342,8 +348,6 @@
|
||||
"allowedIpRanges": "Разрешённые диапазоны IP"
|
||||
},
|
||||
"changePasswordAction": "Изменить пароль",
|
||||
"disable2FAAction": "Выключить 2FA",
|
||||
"enable2FAAction": "Включить 2FA",
|
||||
"passwordResetNotification": {
|
||||
"body": "Письмо отправлено на адрес электронной почты {{ email }}"
|
||||
},
|
||||
@@ -354,6 +358,11 @@
|
||||
"removeAppPassword": {
|
||||
"title": "Удалить пароль приложения",
|
||||
"description": "Удалить пароль приложения \"{{ name }}\" ?"
|
||||
},
|
||||
"twoFactorAuth": {
|
||||
"title": "Двухфакторная аутентификация",
|
||||
"totpEnabled": "Используется одноразовый пароль (TOTP)",
|
||||
"passkeyEnabled": "Используется ключ доступа"
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
@@ -367,7 +376,7 @@
|
||||
"customAppUpdateInfo": "Для сторонних приложений автообновления недоступны.",
|
||||
"description": "Название & версия приложения",
|
||||
"appId": "ID приложения",
|
||||
"packageVersion": "Пакет",
|
||||
"packageVersion": "Версия пакета",
|
||||
"lastUpdated": "Обновлен",
|
||||
"installedAt": "Установлено",
|
||||
"packager": "Сборщик"
|
||||
@@ -377,7 +386,7 @@
|
||||
"description": "Обновления приложения применяются периодически, в соответствии с <a href=\"/#/system-update\">расписанием обновлений</a>"
|
||||
},
|
||||
"updates": {
|
||||
"description": "Cloudron автоматически проверяет Магазин приложений на наличие обновлений. Вы также можете проверить их вручную."
|
||||
"description": "Cloudron автоматически проверяет наличие обновлений для приложений. Вы также можете проверить их вручную."
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -410,8 +419,7 @@
|
||||
"saveAction": "Сохранить",
|
||||
"aliases": "Псевдонимы",
|
||||
"addAliasAction": "Добавить псевдоним",
|
||||
"noAliases": "Домены-псевдонимы отсутствуют",
|
||||
"dnsoverwrite": "Некоторые DNS записи уже существуют. Подтвердите перезапись."
|
||||
"noAliases": "Домены-псевдонимы отсутствуют"
|
||||
},
|
||||
"accessControl": {
|
||||
"sftp": {
|
||||
@@ -644,7 +652,7 @@
|
||||
"cloneDialog": {
|
||||
"title": "Клонировать приложение",
|
||||
"location": "Расположение",
|
||||
"description": "Клон использует резервную копию версии <b>v{{ packageVersion }}</b> от <b>{{ creationTime }}</b>."
|
||||
"description": "Клон использует резервную копию версии <b>{{ packageVersion }}</b> от <b>{{ creationTime }}</b>."
|
||||
},
|
||||
"addApplinkDialog": {
|
||||
"title": "Добавить Внешнюю ссылку"
|
||||
@@ -687,6 +695,16 @@
|
||||
"forumAction": "Форум",
|
||||
"appLink": {
|
||||
"title": "Внешняя ссылка"
|
||||
},
|
||||
"start": {
|
||||
"title": "Старт",
|
||||
"description": "Запустить приложение и сделать его снова доступным.",
|
||||
"action": "Старт"
|
||||
},
|
||||
"stop": {
|
||||
"action": "Стоп",
|
||||
"title": "Стоп",
|
||||
"description": "Остановить приложение, чтобы сохранить ресурсы. Создайте резервную копию перед этим, чтобы сохранить последние изменения."
|
||||
}
|
||||
},
|
||||
"backups": {
|
||||
@@ -796,7 +814,8 @@
|
||||
"size": "Размер",
|
||||
"duration": "Продолжительность резервного копирования",
|
||||
"lastIntegrityCheck": "Последняя проверка целостности",
|
||||
"integrityNever": "никогда"
|
||||
"integrityNever": "никогда",
|
||||
"integrityInProgress": "В процессе"
|
||||
},
|
||||
"backupEdit": {
|
||||
"title": "Редактировать резервную копию",
|
||||
@@ -839,7 +858,8 @@
|
||||
},
|
||||
"useFileAndFileNameEncryption": "Используется шифрование файлов и их имён",
|
||||
"useFileEncryption": "Используется шифрование файлов",
|
||||
"checkIntegrity": "Проверить целостность"
|
||||
"checkIntegrity": "Проверить целостность",
|
||||
"stopIntegrity": "Остановить проверку целостности"
|
||||
},
|
||||
"branding": {
|
||||
"title": "Брендирование",
|
||||
@@ -1026,17 +1046,13 @@
|
||||
"updateAvailableAction": "Доступно обновление",
|
||||
"stopUpdateAction": "Остановить обновление",
|
||||
"description": "Обновления платформы и приложений запускаются с учётом установленного расписания и в соответствии с <a href=\"/#/system-settings\">системным часовым поясом</a>.",
|
||||
"schedule": "Расписание обновлений",
|
||||
"disabled": "Выключено",
|
||||
"onLatest": "последний"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"title": "Настроить расписание автоматических обновлений",
|
||||
"disableCheckbox": "Выключить автоматические обновления",
|
||||
"enableCheckbox": "Включить автоматические обновления",
|
||||
"selectOne": "Выберите по крайней мере один день и время",
|
||||
"days": "Дни",
|
||||
"hours": "Часы",
|
||||
"description": "Установите дни и часы, в которые будет происходить автоматическое обновление платформы и приложений. Убедитесь, что установленное расписание не пересекается с расписанием резервного копирования."
|
||||
},
|
||||
"updateDialog": {
|
||||
@@ -1113,7 +1129,9 @@
|
||||
"changeDashboardDomain": {
|
||||
"title": "Домен панели управления",
|
||||
"changeAction": "Изменить домен",
|
||||
"description": "Изменяет поддомен панели управления \"my\" для выбранного домена"
|
||||
"description": "Изменяет поддомен панели управления \"my\" для выбранного домена",
|
||||
"confirmMessage": "Это действие сбросит ключи доступа для всех пользователей.",
|
||||
"confirmTitle": "Вы точно хотите сменить домен панели управления?"
|
||||
},
|
||||
"domainDialog": {
|
||||
"editTitle": "Редактировать домен",
|
||||
@@ -1171,7 +1189,9 @@
|
||||
"inwxUsername": "Имя пользователя INWX",
|
||||
"inwxPassword": "Пароль INWX",
|
||||
"customNameservers": "Домен использует пользовательские серверы имён (vanity)",
|
||||
"zoneNamePlaceholder": "Необязательно. Если не указано, используется корневой домен."
|
||||
"zoneNamePlaceholder": "Необязательно. Если не указано, используется корневой домен.",
|
||||
"carddavLocation": "Расположение сервера CardDAV",
|
||||
"caldavLocation": "Расположение сервера CalDAV"
|
||||
},
|
||||
"removeDialog": {
|
||||
"title": "Удалить домен",
|
||||
@@ -1258,7 +1278,8 @@
|
||||
"restartApp": "Перезагрузить приложение",
|
||||
"uploadFolder": "Загрузить папку",
|
||||
"openTerminal": "Открыть терминал",
|
||||
"openLogs": "Открыть логи"
|
||||
"openLogs": "Открыть логи",
|
||||
"refresh": "Обновить"
|
||||
},
|
||||
"removeDialog": {
|
||||
"reallyDelete": "Действительно удалить?"
|
||||
@@ -1490,7 +1511,8 @@
|
||||
"errorIncorrectCredentials": "Неправильное имя пользователя или пароль",
|
||||
"errorIncorrect2FAToken": "Неверный 2FA токен",
|
||||
"errorInternal": "Внутренняя ошибка, попробуйте позже",
|
||||
"loginAction": "Войти"
|
||||
"loginAction": "Войти",
|
||||
"usePasskeyAction": "Использовать ключ доступа"
|
||||
},
|
||||
"passwordReset": {
|
||||
"title": "Сброс пароля",
|
||||
@@ -1647,7 +1669,8 @@
|
||||
"title": "Локации резервных копий",
|
||||
"emptyPlaceholder": "Локации отсутствуют",
|
||||
"lastRun": "Последний запуск",
|
||||
"description": "Локации резервных копий указывают на то, где будут сохраняться копии системы и приложений. Резервные копии приложений могут быть восстановлены по-отдельности."
|
||||
"description": "Локации резервных копий указывают на то, где будут сохраняться копии системы и приложений. Резервные копии приложений могут быть восстановлены по-отдельности.",
|
||||
"noAutomaticUpdateBackupWarning": "Не настроено ни одной локации резервных копий для хранения копий автоматических обновлений. Включите \"Хранить бэкапы автоматических обновлений здесь\" по крайней мере в одной локации, чтобы активировать автоматические обновления."
|
||||
},
|
||||
"site": {
|
||||
"removeDialog": {
|
||||
@@ -1688,6 +1711,7 @@
|
||||
"title": "Сервер"
|
||||
},
|
||||
"communityapp": {
|
||||
"installwarning": "Cloudron не проводит аудит приложений, созданных сообществом. Устанавливайте приложения только от проверенных разработчиков. Сторонний код может поставить под угрозу безопасности вашей системы."
|
||||
"installwarning": "Cloudron не проводит аудит приложений, созданных сообществом. Устанавливайте приложения только от проверенных разработчиков. Сторонний код может поставить под угрозу безопасности вашей системы.",
|
||||
"unstablewarning": "Разработчик пометил это приложение как нестабильное."
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,8 +37,6 @@
|
||||
"copyNow": "请复制 API Token。出于安全考虑,这个 API Token 未来不会再显示。"
|
||||
},
|
||||
"changePasswordAction": "修改密码",
|
||||
"disable2FAAction": "停用双因素验证",
|
||||
"enable2FAAction": "启用双因素验证",
|
||||
"title": "个人资料",
|
||||
"primaryEmail": "主要 Email",
|
||||
"passwordRecoveryEmail": "密码恢复 Email",
|
||||
@@ -534,12 +532,9 @@
|
||||
"stopUpdateAction": "停止更新"
|
||||
},
|
||||
"updateScheduleDialog": {
|
||||
"title": "配置自动更新时间表",
|
||||
"disableCheckbox": "停用自动更新",
|
||||
"enableCheckbox": "启用自动更新",
|
||||
"selectOne": "选择至少一个日期和时间",
|
||||
"days": "星期",
|
||||
"hours": "小时",
|
||||
"description": "选择检查平台和应用更新的日子和时间。请注意这个时间不要和 <a href=\"/#/backups\">备份时间</a> 冲突。"
|
||||
},
|
||||
"updateDialog": {
|
||||
|
||||
+26
-13
@@ -8,12 +8,13 @@ import { onMounted, onUnmounted, ref, useTemplateRef, provide } from 'vue';
|
||||
import { Notification, InputDialog, fetcher } from '@cloudron/pankow';
|
||||
import { setLanguage } from './i18n.js';
|
||||
import { API_ORIGIN, TOKEN_TYPES } from './constants.js';
|
||||
import { redirectIfNeeded } from './utils.js';
|
||||
import { redirectIfNeeded, startAuthFlow } from './utils.js';
|
||||
import ProfileModel from './models/ProfileModel.js';
|
||||
import ProvisionModel from './models/ProvisionModel.js';
|
||||
import NotificationsModel from './models/NotificationsModel.js';
|
||||
import DashboardModel from './models/DashboardModel.js';
|
||||
import BrandingModel from './models/BrandingModel.js';
|
||||
import AppstoreModel from './models/AppstoreModel.js';
|
||||
import Headerbar from './components/Headerbar.vue';
|
||||
import SubscriptionRequiredDialog from './components/SubscriptionRequiredDialog.vue';
|
||||
import RequestErrorDialog from './components/RequestErrorDialog.vue';
|
||||
@@ -277,6 +278,7 @@ const dashboardModel = DashboardModel.create();
|
||||
const profileModel = ProfileModel.create();
|
||||
const provisionModel = ProvisionModel.create();
|
||||
const notificationModel = NotificationsModel.create();
|
||||
const appstoreModel = AppstoreModel.create();
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
const subscriptionRequiredDialog = useTemplateRef('subscriptionRequiredDialog');
|
||||
@@ -389,6 +391,9 @@ async function refreshConfigAndFeatures() {
|
||||
console.log('Dashboard version changed, reloading');
|
||||
localStorage.setItem('version', result.version);
|
||||
window.location.reload(true);
|
||||
|
||||
// return never ending promise to just wait for the reload
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
config.value = result;
|
||||
@@ -402,6 +407,14 @@ async function refreshNotifications() {
|
||||
notificationCount.value = result.length;
|
||||
}
|
||||
|
||||
async function refreshSubscription() {
|
||||
const [error, result] = await appstoreModel.getSubscription();
|
||||
if (error && error.status === 402) console.error('Not yet registered');
|
||||
else if (error && error.status === 412) window.location.href = ''
|
||||
else if (error) console.error(error);
|
||||
else subscription.value = result;
|
||||
}
|
||||
|
||||
async function onOnline() {
|
||||
ready.value = true;
|
||||
await refreshConfigAndFeatures(); // reload dashboard if needed after an update
|
||||
@@ -413,6 +426,7 @@ function checkForMobile() {
|
||||
}
|
||||
|
||||
provide('subscriptionRequiredDialog', subscriptionRequiredDialog);
|
||||
provide('subscription', subscription);
|
||||
provide('features', features);
|
||||
provide('profile', profile);
|
||||
provide('refreshProfile', refreshProfile);
|
||||
@@ -433,19 +447,18 @@ onMounted(async () => {
|
||||
if (!localStorage.token) {
|
||||
localStorage.setItem('redirectToHash', window.location.hash);
|
||||
|
||||
// start oidc flow
|
||||
window.location.href = `${API_ORIGIN}/openid/auth?client_id=` + (API_ORIGIN ? TOKEN_TYPES.ID_DEVELOPMENT : TOKEN_TYPES.ID_WEBADMIN) + '&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
const clientId = API_ORIGIN ? TOKEN_TYPES.ID_DEVELOPMENT : TOKEN_TYPES.ID_WEBADMIN;
|
||||
window.location.href = await startAuthFlow(clientId, API_ORIGIN);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await refreshConfigAndFeatures();
|
||||
await refreshProfile();
|
||||
|
||||
// ensure language from profile if set
|
||||
if (profile.value.language) await setLanguage(profile.value.language, true);
|
||||
|
||||
await refreshConfigAndFeatures();
|
||||
|
||||
avatarUrl.value = `https://${config.value.adminFqdn}/api/v1/cloudron/avatar`;
|
||||
|
||||
window.addEventListener('hashchange', onHashChange);
|
||||
@@ -453,16 +466,16 @@ onMounted(async () => {
|
||||
|
||||
console.log(`Cloudron dashboard v${config.value.version}`);
|
||||
|
||||
if (profile.value.isAtLeastAdmin) refreshNotifications();
|
||||
if (profile.value.isAtLeastAdmin) {
|
||||
refreshNotifications();
|
||||
refreshSubscription();
|
||||
}
|
||||
|
||||
ready.value = true;
|
||||
|
||||
// when done, redirect the user to setup 2fa if it is mandatory and neither totp (twoFactorAuthenticationEnabled) nor passkey is setup
|
||||
if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled) {
|
||||
const [error, result] = await profileModel.getPasskey();
|
||||
if (error) return window.cloudron.onError(error);
|
||||
|
||||
if (!result) return window.location.href = VIEWS.PROFILE;
|
||||
// when done, redirect the user to setup 2fa if it is mandatory and neither totp nor passkey is setup
|
||||
if (config.value.mandatory2FA && !profile.value.totpEnabled && !profile.value.hasPasskey) {
|
||||
return window.location.href = VIEWS.PROFILE;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -484,7 +497,7 @@ onUnmounted(() => {
|
||||
<SideBar v-if="profile.isAtLeastUserManager" :items="menuItems" :cloudron-name="config.cloudronName" :cloudron-avatar-url="avatarUrl"/>
|
||||
|
||||
<div style="flex-grow: 1; display: flex; flex-direction: column; overflow: hidden; height: 100%;">
|
||||
<Headerbar :config="config" :subscription="subscription" :notification-count="notificationCount"/>
|
||||
<Headerbar :config="config" :notification-count="notificationCount"/>
|
||||
|
||||
<div style="display: flex; justify-content: center; overflow: auto; flex-grow: 1; padding: 0; margin: 0 10px; position: relative;">
|
||||
<KeepAlive>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { ref, computed, useTemplateRef, onMounted, inject, watch } from 'vue';
|
||||
import { marked } from 'marked';
|
||||
import { Button, Dialog, SingleSelect, FormGroup, TextInput, InputGroup, Spinner } from '@cloudron/pankow';
|
||||
import { Button, Checkbox, Dialog, SingleSelect, FormGroup, TextInput, InputGroup, Spinner } from '@cloudron/pankow';
|
||||
import { prettyDate, prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import AccessControl from './AccessControl.vue';
|
||||
import PortBindings from './PortBindings.vue';
|
||||
@@ -42,6 +42,12 @@ const domains = ref([]);
|
||||
|
||||
const form = ref(null); // assigned via "Function Ref" because it is inside v-if
|
||||
const isFormValid = ref(false);
|
||||
function resetDnsOverwrite() {
|
||||
needsOverwriteDns.value = [];
|
||||
overwriteDns.value = false;
|
||||
formError.value = {};
|
||||
}
|
||||
|
||||
async function checkValidity() {
|
||||
isFormValid.value = form.value ? form.value.checkValidity() : false;
|
||||
|
||||
@@ -89,7 +95,8 @@ const tcpPorts = ref({});
|
||||
const udpPorts = ref({});
|
||||
const secondaryDomains = ref({});
|
||||
const upstreamUri = ref('');
|
||||
const needsOverwriteDns = ref(false);
|
||||
const overwriteDns = ref(false);
|
||||
const needsOverwriteDns = ref([]);
|
||||
const users = ref([]);
|
||||
const groups = ref([]);
|
||||
|
||||
@@ -98,7 +105,7 @@ function onDomainChange() {
|
||||
domainProvider.value = tmp ? tmp.provider : '';
|
||||
}
|
||||
|
||||
async function onSubmit(overwriteDns) {
|
||||
async function onSubmit() {
|
||||
if (!form.value.reportValidity()) return;
|
||||
|
||||
formError.value = {};
|
||||
@@ -111,6 +118,7 @@ async function onSubmit(overwriteDns) {
|
||||
|
||||
for (const d in secondaryDomains.value) checkForDomains.push({ domain: secondaryDomains.value[d].domain, subdomain: secondaryDomains.value[d].value });
|
||||
|
||||
const conflicting = [];
|
||||
for (const d of checkForDomains) {
|
||||
const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
|
||||
if (error) {
|
||||
@@ -119,12 +127,14 @@ async function onSubmit(overwriteDns) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
if (result.needsOverwrite && !overwriteDns) {
|
||||
busy.value = false;
|
||||
needsOverwriteDns.value = true;
|
||||
formError.value.dnsExists = `DNS record for ${d.subdomain}.${d.domain} already exists`;
|
||||
return;
|
||||
}
|
||||
if (result.needsOverwrite) conflicting.push((d.subdomain ? d.subdomain + '.' : '') + d.domain);
|
||||
}
|
||||
|
||||
if (conflicting.length > 0 && !overwriteDns.value) {
|
||||
busy.value = false;
|
||||
needsOverwriteDns.value = conflicting;
|
||||
formError.value.generic = `DNS records of ${conflicting.join(', ')} already exist`;
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {
|
||||
@@ -133,7 +143,7 @@ async function onSubmit(overwriteDns) {
|
||||
accessRestriction: accessRestrictionOption.value === ACL_OPTIONS.ANY ? null : (accessRestrictionOption.value === ACL_OPTIONS.NOSSO ? null : accessRestrictionAcl.value)
|
||||
};
|
||||
|
||||
if (overwriteDns) config.overwriteDns = true;
|
||||
if (overwriteDns.value) config.overwriteDns = true;
|
||||
|
||||
if (manifest.value.optionalSso) config.sso = accessRestrictionOption.value !== ACL_OPTIONS.NOSSO;
|
||||
|
||||
@@ -185,7 +195,7 @@ function onClose() {
|
||||
onMounted(async () => {
|
||||
let [error, result] = await usersModel.list();
|
||||
if (error) return console.error(error);
|
||||
result.forEach(u => { u.label = u.displayName || u.username || u.email });
|
||||
result.forEach(u => { u.label = u.displayName || u.username || u.email; });
|
||||
users.value = result;
|
||||
|
||||
[error, result] = await groupsModel.list();
|
||||
@@ -225,7 +235,8 @@ defineExpose({
|
||||
accessRestrictionAcl.value = { users: [], groups: [] };
|
||||
domainProvider.value = '';
|
||||
upstreamUri.value = '';
|
||||
needsOverwriteDns.value = '';
|
||||
overwriteDns.value = false;
|
||||
needsOverwriteDns.value = [];
|
||||
|
||||
domainList.forEach(d => {
|
||||
d.label = '.' + d.domain;
|
||||
@@ -296,18 +307,15 @@ defineExpose({
|
||||
<div class="description" v-html="description"></div>
|
||||
</div>
|
||||
<div v-else-if="step === STEP.INSTALL">
|
||||
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
<div class="error-label" v-if="formError.dnsExists">{{ formError.dnsExists }}</div>
|
||||
|
||||
<form :ref="(el) => { form = el; }" @submit.prevent="onSubmit(false)" autocomplete="off" @input="checkValidity()">
|
||||
<form :ref="(el) => { form = el; }" @submit.prevent="onSubmit()" autocomplete="off" @input="checkValidity()">
|
||||
<fieldset :disabled="busy">
|
||||
<input style="display: none;" type="submit" :disabled="busy" />
|
||||
|
||||
<FormGroup :class="{ 'has-error': formError.location }">
|
||||
<label for="location">{{ $t('appstore.installDialog.location') }}</label>
|
||||
<InputGroup>
|
||||
<TextInput id="location" ref="locationInput" v-model="location" style="flex-grow: 1"/>
|
||||
<SingleSelect v-model="domain" :options="domains" option-label="label" option-key="domain" @select="onDomainChange()" :search-threshold="10" required/>
|
||||
<TextInput id="location" ref="locationInput" v-model="location" @input="resetDnsOverwrite()" style="flex-grow: 1"/>
|
||||
<SingleSelect v-model="domain" :options="domains" option-label="label" option-key="domain" @select="onDomainChange(); resetDnsOverwrite()" :search-threshold="10" required/>
|
||||
</InputGroup>
|
||||
<div class="warning-label" v-show="domainProvider === 'noop' || domainProvider === 'manual'" v-html="$t('appstore.installDialog.manualWarning', { location: ((location ? location + '.' : '') + domain) })"></div>
|
||||
<div class="error-label" v-if="formError.location">{{ formError.location }}</div>
|
||||
@@ -317,8 +325,8 @@ defineExpose({
|
||||
<label :for="'secondaryDomainInput' + key">{{ port.title }}</label>
|
||||
<small>{{ port.description }}</small>
|
||||
<InputGroup>
|
||||
<TextInput :id="'secondaryDomainInput' + key" v-model="port.value" :placeholder="$t('appstore.installDialog.locationPlaceholder')" style="flex-grow: 1"/>
|
||||
<SingleSelect v-model="port.domain" :options="domains" option-label="label" option-key="domain" required/>
|
||||
<TextInput :id="'secondaryDomainInput' + key" v-model="port.value" @input="resetDnsOverwrite()" :placeholder="$t('appstore.installDialog.locationPlaceholder')" style="flex-grow: 1"/>
|
||||
<SingleSelect v-model="port.domain" :options="domains" option-label="label" option-key="domain" @select="resetDnsOverwrite()" required/>
|
||||
</InputGroup>
|
||||
</FormGroup>
|
||||
|
||||
@@ -330,9 +338,13 @@ defineExpose({
|
||||
<PortBindings v-model:tcp="tcpPorts" v-model:udp="udpPorts" :error="formError" :domain-provider="domainProvider"/>
|
||||
<AccessControl v-model:option="accessRestrictionOption" v-model:acl="accessRestrictionAcl" :manifest="manifest" :users="users" :groups="groups" :installation="true"/>
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="text-danger" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
<Checkbox v-if="needsOverwriteDns.length" v-model="overwriteDns" style="margin-top: 10px" :label="$t('app.location.overwriteDns', { domains: needsOverwriteDns.join(', ') })"/>
|
||||
|
||||
<div class="bottom-button-bar">
|
||||
<Button v-if="needsOverwriteDns" danger @click="onSubmit(true)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }} and overwrite DNS</Button>
|
||||
<Button v-else @click="onSubmit(false)" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid" :loading="busy">Install {{ manifest.title }}</Button>
|
||||
<Button @click="onSubmit()" icon="fa-solid fa-circle-down" :disabled="busy || !isFormValid || (needsOverwriteDns.length > 0 && !overwriteDns)" :loading="busy">Install {{ manifest.title }}</Button>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
|
||||
@@ -172,12 +172,6 @@ onMounted(async () => {
|
||||
<SingleSelect id="blockDevicePath" v-if="provider === 'xfs'" v-model="providerConfig.mountOptionDiskPath" :options="xfsBlockDevices" option-label="label" option-key="path"/>
|
||||
</FormGroup>
|
||||
|
||||
<!-- Disk -->
|
||||
<FormGroup v-if="provider === 'disk'">
|
||||
<label class="control-label">{{ $t('backups.configureBackupStorage.diskPath') }}</label>
|
||||
<TextInput id="mountOptionDiskPathInput" v-model="providerConfig.mountOptionDiskPath" placeholder="/dev/disk/by-uuid/uuid" required />
|
||||
</FormGroup>
|
||||
|
||||
<!-- SSHFS -->
|
||||
<FormGroup v-if="provider === 'sshfs'">
|
||||
<label for="mountOptionPortInput">{{ $t('backups.configureBackupStorage.port') }}</label>
|
||||
|
||||
@@ -124,7 +124,7 @@ async function onSubmit() {
|
||||
data.mountOptions.privateKey = providerConfig.value.mountOptionPrivateKey;
|
||||
data.preserveAttributes = true;
|
||||
}
|
||||
} else if (provider.value === 'ext4' || provider.value === 'xfs' || provider.value === 'disk') {
|
||||
} else if (provider.value === 'ext4' || provider.value === 'xfs') {
|
||||
data.mountOptions.diskPath = providerConfig.value.mountOptionDiskPath;
|
||||
data.preserveAttributes = true;
|
||||
} else if (provider.value === 'mountpoint') {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ref, useTemplateRef, watch } from 'vue';
|
||||
import { MaskedInput, Dialog, FormGroup, TextInput, Checkbox } from '@cloudron/pankow';
|
||||
import { prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import { s3like, mountlike, prettySiteLocation } from '../utils.js';
|
||||
import { s3like, mountlike } from '../utils.js';
|
||||
import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import SystemModel from '../models/SystemModel.js';
|
||||
|
||||
@@ -205,7 +205,7 @@ defineExpose({
|
||||
|
||||
<FormGroup v-if="site.provider && site.config">
|
||||
<label><i v-if="site.encrypted" class="fa-solid fa-lock"></i> Storage: <b>{{ site.provider }} ({{ site.format }}) </b></label>
|
||||
<div>{{ prettySiteLocation(site) }}</div>
|
||||
<div>{{ site.locationLabel }}</div>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup v-if="provider === 'sshfs'">
|
||||
|
||||
@@ -10,6 +10,7 @@ const emit = defineEmits([ 'success' ]);
|
||||
|
||||
const dialog = useTemplateRef('dialog');
|
||||
const form = useTemplateRef('form');
|
||||
const urlInput = useTemplateRef('urlInput');
|
||||
|
||||
const formError = ref({});
|
||||
const versionsUrl = ref('');
|
||||
@@ -56,6 +57,7 @@ defineExpose({
|
||||
unstable.value = false;
|
||||
dialog.value.open();
|
||||
setTimeout(validateForm, 100); // update state of the confirm button
|
||||
setTimeout(() => urlInput.value.focus(), 500);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -81,7 +83,7 @@ defineExpose({
|
||||
|
||||
<FormGroup>
|
||||
<label for="urlInput">CloudronVersions.json URL</label>
|
||||
<TextInput id="urlInput" v-model="versionsUrl" required placeholder="https://example.com/CloudronVersions.json"/>
|
||||
<TextInput id="urlInput" ref="urlInput" v-model="versionsUrl" required placeholder="https://example.com/CloudronVersions.json"/>
|
||||
<div class="error-label" v-if="formError.generic">{{ formError.generic }}</div>
|
||||
</FormGroup>
|
||||
</fieldset>
|
||||
|
||||
@@ -71,7 +71,7 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<Dialog ref="dialog"
|
||||
:title="$t('profile.disable2FA.title')"
|
||||
:title="twoFAMethod === 'totp' ? $t('profile.disableTotp.title') : $t('profile.disablePasskey.title')"
|
||||
:confirm-label="$t('profile.disable2FA.disable')"
|
||||
:confirm-active="!busy && isFormValid"
|
||||
:confirm-busy="busy"
|
||||
|
||||
@@ -319,7 +319,7 @@ function onGcdnsFileInputChange(event) {
|
||||
<Checkbox v-if="showAdvanced" v-model="customNameservers" :label="$t('domains.domainDialog.customNameservers')" />
|
||||
|
||||
<FormGroup v-if="showAdvanced">
|
||||
<label>Certificate provider <sup><a href="https://docs.cloudron.io/certificates/#certificate-providers" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<label>Certificate provider <sup><a href="https://docs.cloudron.io/domains#certificates" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
<SingleSelect v-model="tlsProvider" :options="tlsProviders" option-key="value" option-label="name" required/>
|
||||
</FormGroup>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, useTemplateRef } from 'vue';
|
||||
import { Button, ButtonGroup, ClipboardAction, Dialog, TextInput, InputGroup, FormGroup } from '@cloudron/pankow';
|
||||
import { Button, ClipboardAction, Dialog, TextInput, InputGroup, FormGroup } from '@cloudron/pankow';
|
||||
import { startRegistration } from '@simplewebauthn/browser';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
|
||||
@@ -64,37 +64,42 @@ async function onRegisterPasskey() {
|
||||
passkeyRegisterBusy.value = false;
|
||||
}
|
||||
|
||||
async function loadTotpSecret() {
|
||||
const [error, result] = await profileModel.setTotpSecret();
|
||||
if (error) return console.error(error);
|
||||
|
||||
totpSecret.value = result.secret;
|
||||
totpQRCode.value = result.qrcode;
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
async open() {
|
||||
setupMode.value = 'passkey';
|
||||
async open(method) {
|
||||
setupMode.value = method || 'passkey';
|
||||
totpEnableError.value = '';
|
||||
totpToken.value = '';
|
||||
passkeyRegisterError.value = '';
|
||||
|
||||
dialog.value.open();
|
||||
|
||||
const [error, result] = await profileModel.setTotpSecret();
|
||||
if (error) return console.error(error);
|
||||
|
||||
totpSecret.value = result.secret;
|
||||
totpQRCode.value = result.qrcode;
|
||||
if (setupMode.value === 'totp') await loadTotpSecret();
|
||||
},
|
||||
close() {
|
||||
dialog.value.close();
|
||||
}
|
||||
});
|
||||
|
||||
async function switchMode(mode) {
|
||||
setupMode.value = mode;
|
||||
if (mode === 'totp' && !totpSecret.value) await loadTotpSecret();
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Dialog ref="dialog" :title="$t('profile.enable2FA.title')" :dismissable="!props.mandatory2FA || props.has2FA">
|
||||
<Dialog ref="dialog" :title="setupMode === 'totp' ? $t('profile.enableTotp.title') : $t('profile.enablePasskey.title')" :dismissable="!props.mandatory2FA || props.has2FA">
|
||||
<div>
|
||||
<p class="text-warning" v-if="props.mandatory2FA && !props.has2FA">{{ $t('profile.enable2FA.mandatorySetup') }}</p>
|
||||
|
||||
<ButtonGroup style="display: flex; justify-content: center;"> <Button secondary @click="setupMode = 'passkey'" :outline="setupMode !== 'passkey' || null">{{ $t('profile.enable2FA.passkeyOption') }}</Button>
|
||||
<Button secondary @click="setupMode = 'totp'" :outline="setupMode !== 'totp' || null">{{ $t('profile.enable2FA.totpOption') }}</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<!-- Passkey Setup -->
|
||||
<div v-if="setupMode === 'passkey'">
|
||||
<p v-html="$t('profile.enable2FA.passkeyDescription', { googleAuthenticatorPlayStoreLink: 'https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2', googleAuthenticatorITunesLink: 'https://itunes.apple.com/us/app/google-authenticator/id388497605', freeOTPPlayStoreLink: 'https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp', freeOTPITunesLink: 'https://itunes.apple.com/us/app/freeotp-authenticator/id872559395'})"></p>
|
||||
@@ -102,6 +107,9 @@ defineExpose({
|
||||
<Button @click="onRegisterPasskey()" :loading="passkeyRegisterBusy" :disabled="passkeyRegisterBusy">{{ $t('profile.enable2FA.registerPasskey') }}</Button>
|
||||
<div class="error-label" v-if="passkeyRegisterError">{{ passkeyRegisterError }}</div>
|
||||
</div>
|
||||
<p v-if="props.mandatory2FA && !props.has2FA" style="text-align: center; margin-top: 15px;">
|
||||
<a href="#" @click.prevent="switchMode('totp')">{{ $t('profile.enable2FA.switchToTotp') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- TOTP Setup -->
|
||||
@@ -123,6 +131,9 @@ defineExpose({
|
||||
<div class="error-label" v-if="totpEnableError">{{ totpEnableError }}</div>
|
||||
</FormGroup>
|
||||
</form>
|
||||
<p v-if="props.mandatory2FA && !props.has2FA" style="text-align: center; margin-top: 15px;">
|
||||
<a href="#" @click.prevent="switchMode('passkey')">{{ $t('profile.enable2FA.switchToPasskey') }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick, useTemplateRef } from 'vue';
|
||||
import { ref, computed, onMounted, watch, nextTick, useTemplateRef } from 'vue';
|
||||
import { Button, TextInput, MultiSelect, Popover, FormGroup, DateTimeInput } from '@cloudron/pankow';
|
||||
import { useDebouncedRef, prettyLongDate } from '@cloudron/pankow/utils';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
@@ -21,7 +21,7 @@ const refreshBusy = ref(false);
|
||||
const page = ref(1);
|
||||
const perPage = ref(100);
|
||||
const eventlogContainer = useTemplateRef('eventlogContainer');
|
||||
const actions = reactive([]);
|
||||
const actions = ref([]);
|
||||
|
||||
const highlight = useDebouncedRef('', 300);
|
||||
const currentMatchPosition = ref(-1);
|
||||
@@ -107,7 +107,7 @@ async function goToNextMatch() {
|
||||
|
||||
function buildFilter() {
|
||||
const filter = {};
|
||||
if (actions.length) filter.actions = actions.join(',');
|
||||
if (actions.value.length) filter.actions = actions.value.join(',');
|
||||
if (filterFrom.value) filter.from = new Date(filterFrom.value + 'T00:00:00').toISOString();
|
||||
if (filterTo.value) filter.to = new Date(filterTo.value + 'T23:59:59.999').toISOString();
|
||||
return filter;
|
||||
@@ -142,7 +142,7 @@ function onOpenDateFilter(event) {
|
||||
dateFilterPopover.value.open(event, dateFilterButton.value.$el || dateFilterButton.value);
|
||||
}
|
||||
|
||||
watch(actions, onRefresh);
|
||||
watch(actions.value, onRefresh);
|
||||
watch(filterFrom, onRefresh);
|
||||
watch(filterTo, onRefresh);
|
||||
watch(highlight, async () => {
|
||||
@@ -202,8 +202,8 @@ defineExpose({ refresh: onRefresh, setHighlight });
|
||||
<table class="eventlog-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 160px;">{{ $t('eventlog.time') }}</th>
|
||||
<th style="width: 15%;">{{ $t('eventlog.source') }}</th>
|
||||
<th style="width: 190px;">{{ $t('eventlog.time') }}</th>
|
||||
<th style="width: 100px;">{{ $t('eventlog.source') }}</th>
|
||||
<th>{{ $t('eventlog.details') }}</th>
|
||||
<th style="width: 40px;"></th>
|
||||
</tr>
|
||||
@@ -211,10 +211,10 @@ defineExpose({ refresh: onRefresh, setHighlight });
|
||||
<tbody>
|
||||
<template v-for="(eventlog, index) in eventlogs" :key="eventlog.id">
|
||||
<tr :data-index="index" :class="{ 'active': eventlog.isOpen, 'eventlog-match': highlight && isMatch(eventlog, highlight), 'eventlog-match-current': matchIndices[currentMatchPosition] === index }" @click="eventlog.isOpen = !eventlog.isOpen">
|
||||
<td style="white-space: nowrap;">{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
|
||||
<td>{{ prettyLongDate(eventlog.raw.creationTime) }}</td>
|
||||
<td class="eventlog-source">{{ eventlog.source }}</td>
|
||||
<td style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" v-html="eventlog.details"></td>
|
||||
<td><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="`/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></td>
|
||||
<td v-html="eventlog.details"></td>
|
||||
<td><Button v-if="eventlog.raw.data.taskId" @click.stop plain small tool :href="app ? `/logs.html?appId=${app.id}&taskId=${eventlog.raw.data.taskId}` : `/logs.html?taskId=${eventlog.raw.data.taskId}`" target="_blank">Logs</Button></td>
|
||||
</tr>
|
||||
<tr v-show="eventlog.isOpen">
|
||||
<td colspan="4" class="eventlog-details" @click.stop>
|
||||
@@ -243,6 +243,9 @@ defineExpose({ refresh: onRefresh, setHighlight });
|
||||
.eventlog-table th,
|
||||
.eventlog-table td {
|
||||
padding: 6px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.eventlog-table tbody tr.active,
|
||||
|
||||
@@ -175,37 +175,66 @@ async function onDrop(targetFolder, dataTransfer, files) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readEntries(dirReader) {
|
||||
return new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(resolve, reject);
|
||||
});
|
||||
function setRelativePath(file, entry) {
|
||||
const relativePath = (entry.fullPath || entry.name || '').replace(/^\//, '');
|
||||
if (relativePath) {
|
||||
// trying with defineProperty() to better mimic native behavior adding a non-enumeratible property
|
||||
try {
|
||||
Object.defineProperty(file, 'webkitRelativePath', { value: relativePath });
|
||||
} catch {
|
||||
file.webkitRelativePath = relativePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// wrapper as chrome only returns files in batches of 100 entries
|
||||
async function readAllEntries(dirReader) {
|
||||
const all = [];
|
||||
let batch;
|
||||
do {
|
||||
batch = await new Promise((resolve, reject) => {
|
||||
dirReader.readEntries(resolve, reject);
|
||||
});
|
||||
all.push(...batch);
|
||||
} while (batch.length > 0);
|
||||
return all;
|
||||
}
|
||||
|
||||
const fileList = [];
|
||||
async function traverseFileTree(item) {
|
||||
if (item.isFile) {
|
||||
fileList.push(await getFile(item));
|
||||
const file = await getFile(item);
|
||||
setRelativePath(file, item);
|
||||
fileList.push(file);
|
||||
} else if (item.isDirectory) {
|
||||
// Get folder contents
|
||||
const dirReader = item.createReader();
|
||||
const entries = await readEntries(dirReader);
|
||||
const entries = await readAllEntries(dirReader);
|
||||
|
||||
for (const i in entries) {
|
||||
await traverseFileTree(entries[i], item.name);
|
||||
await traverseFileTree(entries[i]);
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping uknown file type', item);
|
||||
console.log('Skipping unknown file type', item);
|
||||
}
|
||||
}
|
||||
|
||||
// collect all files to upload
|
||||
for (const item of dataTransfer.items) {
|
||||
const entry = item.webkitGetAsEntry();
|
||||
const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
|
||||
|
||||
if (!entry) {
|
||||
const file = item.getAsFile ? item.getAsFile() : null;
|
||||
if (file) fileList.push(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile) {
|
||||
fileList.push(await getFile(entry));
|
||||
const file = await getFile(entry);
|
||||
setRelativePath(file, entry);
|
||||
fileList.push(file);
|
||||
} else if (entry.isDirectory) {
|
||||
await traverseFileTree(entry, sanitize(`${cwd.value}/${targetFolder}`));
|
||||
await traverseFileTree(entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,9 +10,10 @@ import { Menu, Popover, Icon, InputDialog, Spinner } from '@cloudron/pankow';
|
||||
import ServicesModel from '../models/ServicesModel.js';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
|
||||
defineProps(['config', 'subscription', 'notificationCount']);
|
||||
defineProps(['config', 'notificationCount']);
|
||||
|
||||
const profile = inject('profile');
|
||||
const subscription = inject('subscription');
|
||||
|
||||
const helpButton = useTemplateRef('helpButton');
|
||||
const helpPopover = useTemplateRef('helpPopover');
|
||||
@@ -115,8 +116,9 @@ onUnmounted(() => {
|
||||
<Icon :icon="'fa fa-exclamation-triangle'"/> {{ $t('main.platform.startupFailed') }}
|
||||
</div>
|
||||
|
||||
<!-- Warnings if subscription is expired or unpaid -->
|
||||
<div v-if="profile.isAtLeastOwner && subscription.plan.id === 'expired'" class="headerbar-action subscription-expired" style="gap: 6px" @click="onSubscriptionRequired()">Subscription Expired</div>
|
||||
<!-- Warnings if subscription is expired, unpaid or canceled -->
|
||||
<a v-if="profile.isAtLeastOwner && subscription.plan.id === 'expired'" class="headerbar-action subscription-expired" href="/#/cloudron-account">Subscription Expired</a>
|
||||
<a v-else-if="profile.isAtLeastOwner && (subscription.cancel_at || subscription.status === 'canceled')" class="headerbar-action subscription-canceled" href="/#/cloudron-account">Subscription Canceled</a>
|
||||
|
||||
<a class="headerbar-action" v-if="profile.isAtLeastAdmin" href="/#/notifications"><Icon :icon="notificationCount > 0 ? 'fas fa-bell' : 'far fa-bell'"/> {{ notificationCount > 99 ? '99+' : notificationCount }}</a>
|
||||
<div class="headerbar-action pankow-no-mobile" v-if="profile.isAtLeastAdmin" ref="helpButton" @click="onOpenHelp(helpPopover, $event, helpButton)"><Icon icon="fa fa-question"/></div>
|
||||
@@ -168,13 +170,16 @@ onUnmounted(() => {
|
||||
border-bottom: 1px solid var(--pankow-input-border-color);
|
||||
}
|
||||
|
||||
.subscription-expired {
|
||||
.subscription-expired,
|
||||
.subscription-canceled {
|
||||
background-color: var(--pankow-color-danger);
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.subscription-expired:hover {
|
||||
.subscription-expired:hover,
|
||||
.subscription-canceled:hover {
|
||||
color: white;
|
||||
background-color: var(--pankow-color-danger-hover);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,11 @@ onMounted(async () => {
|
||||
const crashId = urlParams.get('crashId');
|
||||
const idParam = urlParams.get('id');
|
||||
|
||||
if (appId) {
|
||||
if (appId && taskId) {
|
||||
type.value = 'task';
|
||||
id.value = taskId;
|
||||
name.value = 'Task ' + taskId;
|
||||
} else if (appId) {
|
||||
type.value = 'app';
|
||||
id.value = appId;
|
||||
name.value = 'App ' + appId;
|
||||
@@ -89,7 +93,7 @@ onMounted(async () => {
|
||||
return;
|
||||
}
|
||||
|
||||
logsModel = LogsModel.create(type.value, id.value);
|
||||
logsModel = LogsModel.create(type.value, id.value, { appId });
|
||||
|
||||
if (type.value === 'app') {
|
||||
const [error, app] = await appsModel.get(id.value);
|
||||
|
||||
@@ -13,9 +13,11 @@ const appUpp = ref(false);
|
||||
const appDown = ref(false);
|
||||
const appOutOfMemory = ref(false);
|
||||
const backupFailed = ref(false);
|
||||
const appAutoUpdateFailed = ref(false);
|
||||
const certificateRenewalFailed = ref(false);
|
||||
const diskSpace = ref(false);
|
||||
const cloudronUpdateFailed = ref(false);
|
||||
const manualUpdateRequired = ref(false);
|
||||
const reboot = ref(false);
|
||||
|
||||
async function onSubmit() {
|
||||
@@ -26,9 +28,11 @@ async function onSubmit() {
|
||||
if (appDown.value) config.push('appDown');
|
||||
if (appOutOfMemory.value) config.push('appOutOfMemory');
|
||||
if (backupFailed.value) config.push('backupFailed');
|
||||
if (appAutoUpdateFailed.value) config.push('appAutoUpdateFailed');
|
||||
if (certificateRenewalFailed.value) config.push('certificateRenewalFailed');
|
||||
if (diskSpace.value) config.push('diskSpace');
|
||||
if (cloudronUpdateFailed.value) config.push('cloudronUpdateFailed');
|
||||
if (manualUpdateRequired.value) config.push('manualUpdateRequired');
|
||||
if (reboot.value) config.push('reboot');
|
||||
|
||||
const [error] = await profileModel.setNotificationConfig(config);
|
||||
@@ -49,9 +53,11 @@ async function open() {
|
||||
appDown.value = config.indexOf('appDown') !== -1;
|
||||
appOutOfMemory.value = config.indexOf('appOutOfMemory') !== -1;
|
||||
backupFailed.value = config.indexOf('backupFailed') !== -1;
|
||||
appAutoUpdateFailed.value = config.indexOf('appAutoUpdateFailed') !== -1;
|
||||
certificateRenewalFailed.value = config.indexOf('certificateRenewalFailed') !== -1;
|
||||
diskSpace.value = config.indexOf('diskSpace') !== -1;
|
||||
cloudronUpdateFailed.value = config.indexOf('cloudronUpdateFailed') !== -1;
|
||||
manualUpdateRequired.value = config.indexOf('manualUpdateRequired') !== -1;
|
||||
reboot.value = config.indexOf('reboot') !== -1;
|
||||
|
||||
dialogItem.value.open();
|
||||
@@ -98,6 +104,11 @@ defineExpose({
|
||||
<Switch v-model="backupFailed" :disabled="busy"/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem>
|
||||
<div>{{ $t('notifications.settings.appAutoUpdateFailed') }}</div>
|
||||
<Switch v-model="appAutoUpdateFailed" :disabled="busy"/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem>
|
||||
<div>{{ $t('notifications.settings.certificateRenewalFailed') }}</div>
|
||||
<Switch v-model="certificateRenewalFailed" :disabled="busy"/>
|
||||
@@ -113,6 +124,11 @@ defineExpose({
|
||||
<Switch v-model="cloudronUpdateFailed" :disabled="busy"/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem>
|
||||
<div>{{ $t('notifications.settings.manualUpdateRequired') }}</div>
|
||||
<Switch v-model="manualUpdateRequired" :disabled="busy"/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem>
|
||||
<div>{{ $t('notifications.settings.rebootRequired') }}</div>
|
||||
<Switch v-model="reboot" :disabled="busy"/>
|
||||
|
||||
@@ -105,6 +105,7 @@ onUnmounted(() => {
|
||||
margin-bottom: 15px;
|
||||
padding: 10px 15px;
|
||||
padding-bottom: 25px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.section-header-title-badge {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { marked } from 'marked';
|
||||
import { Button, PasswordInput, FormGroup, TextInput } from '@cloudron/pankow';
|
||||
import PublicPageLayout from '../components/PublicPageLayout.vue';
|
||||
import ProfileModel from '../models/ProfileModel.js';
|
||||
import { startAuthFlow } from '../utils.js';
|
||||
|
||||
const profileModel = ProfileModel.create();
|
||||
|
||||
@@ -46,7 +47,7 @@ function validateForm() {
|
||||
}
|
||||
|
||||
async function onSubmit() {
|
||||
if (!form.value.reportValidity()) return;
|
||||
if (!form.value.reportValidity() || !isFormValid.value) return;
|
||||
|
||||
busy.value = true;
|
||||
formError.value = {};
|
||||
@@ -89,7 +90,7 @@ async function onSubmit() {
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = result.accessToken;
|
||||
|
||||
dashboardUrl.value = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
dashboardUrl.value = await startAuthFlow('cid-webadmin', '');
|
||||
|
||||
busy.value = false;
|
||||
mode.value = MODE.DONE;
|
||||
|
||||
@@ -7,14 +7,14 @@ defineProps({
|
||||
},
|
||||
state: {
|
||||
validator(value) {
|
||||
// The value must match one of these strings
|
||||
return ['success', 'warning', 'danger', ''].includes(value);
|
||||
return ['success', 'warning', 'danger', 'idle', ''].includes(value);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
function color(state) {
|
||||
if (state === 'success') return '#27CE65';
|
||||
else if (state === 'idle') return '#BCD0C3';
|
||||
else if (state === 'warning') return '#f0ad4e';
|
||||
else if (state === 'danger') return '#d9534f';
|
||||
else return '#7c7c7c';
|
||||
|
||||
@@ -30,7 +30,8 @@ const taskLogsMenu = ref([]);
|
||||
const apps = ref([]);
|
||||
const version = ref('');
|
||||
const ubuntuVersion = ref('');
|
||||
const currentPattern = ref('');
|
||||
const currentSchedule = ref('');
|
||||
const currentPolicy = ref('');
|
||||
const updateBusy = ref(false);
|
||||
const updateError = ref({});
|
||||
const stopError = ref({});
|
||||
@@ -55,17 +56,16 @@ const inProgressApps = computed(() => {
|
||||
const configureDialog = useTemplateRef('configureDialog');
|
||||
const configureBusy = ref(false);
|
||||
const configureError = ref('');
|
||||
const configureType = ref('');
|
||||
const configurePattern = ref('');
|
||||
const configurePolicy = ref('');
|
||||
const configureDays = ref([]);
|
||||
const configureHours = ref([]);
|
||||
|
||||
async function refreshAutoupdatePattern() {
|
||||
const [error, result] = await updaterModel.getAutoupdatePattern();
|
||||
async function refreshAutoupdateConfig() {
|
||||
const [error, result] = await updaterModel.getAutoupdateConfig();
|
||||
if (error) return console.error(error);
|
||||
|
||||
currentPattern.value = result.pattern;
|
||||
configurePattern.value = result.pattern;
|
||||
currentSchedule.value = result.schedule;
|
||||
currentPolicy.value = result.policy;
|
||||
}
|
||||
|
||||
async function refreshApps() {
|
||||
@@ -87,21 +87,22 @@ async function refreshPendingUpdateInfo() {
|
||||
}
|
||||
|
||||
function onShowConfigure() {
|
||||
if (currentPattern.value === 'never') {
|
||||
configureType.value = 'never';
|
||||
} else {
|
||||
configureType.value = 'pattern';
|
||||
const result = parseSchedule(currentPattern.value);
|
||||
configureDays.value = result.days; // Array of cronDays.id
|
||||
configureHours.value = result.hours; // Array of cronHours.id
|
||||
configurePolicy.value = currentPolicy.value || 'never';
|
||||
|
||||
if (currentPolicy.value !== 'never') {
|
||||
const result = parseSchedule(currentSchedule.value);
|
||||
configureDays.value = result.days;
|
||||
configureHours.value = result.hours;
|
||||
}
|
||||
|
||||
configureDialog.value.open();
|
||||
}
|
||||
|
||||
async function onSubmitConfigure() {
|
||||
let pattern = 'never';
|
||||
if (configureType.value === 'pattern') {
|
||||
let schedule = currentSchedule.value || '00 00 1,3,5,23 * * *';
|
||||
const policy = configurePolicy.value;
|
||||
|
||||
if (policy !== 'never') {
|
||||
let daysPattern;
|
||||
if (configureDays.value.length === 7) daysPattern = '*';
|
||||
else daysPattern = configureDays.value.join(',');
|
||||
@@ -110,18 +111,18 @@ async function onSubmitConfigure() {
|
||||
if (configureHours.value.length === 24) hoursPattern = '*';
|
||||
else hoursPattern = configureHours.value.join(',');
|
||||
|
||||
pattern ='00 00 ' + hoursPattern + ' * * ' + daysPattern;
|
||||
schedule = '00 00 ' + hoursPattern + ' * * ' + daysPattern;
|
||||
}
|
||||
|
||||
configureBusy.value = true;
|
||||
const [error] = await updaterModel.setAutoupdatePattern(pattern);
|
||||
const [error] = await updaterModel.setAutoupdateConfig(schedule, policy);
|
||||
if (error) {
|
||||
configureError.value = error.body ? error.body.message : 'Internal error';
|
||||
configureBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await refreshAutoupdatePattern();
|
||||
await refreshAutoupdateConfig();
|
||||
|
||||
configureBusy.value = false;
|
||||
configureDialog.value.close();
|
||||
@@ -239,7 +240,7 @@ onMounted(async () => {
|
||||
ubuntuVersion.value = result.ubuntuVersion;
|
||||
|
||||
await refreshPendingUpdateInfo();
|
||||
await refreshAutoupdatePattern();
|
||||
await refreshAutoupdateConfig();
|
||||
await refreshTasks();
|
||||
|
||||
ready.value = true;
|
||||
@@ -288,25 +289,35 @@ onMounted(async () => {
|
||||
</Dialog>
|
||||
|
||||
<Dialog ref="configureDialog"
|
||||
:title="$t('settings.updateScheduleDialog.title')"
|
||||
:title="$t('settings.configureUpdates.title')"
|
||||
:confirm-label="$t('main.dialog.save')"
|
||||
:confirm-active="configureType === 'never' ? true : (configureHours.length > 0 && configureDays.length > 0)"
|
||||
:confirm-active="configurePolicy === 'never' ? true : (configureHours.length > 0 && configureDays.length > 0)"
|
||||
:confirm-busy="configureBusy"
|
||||
:reject-label="$t('main.dialog.cancel')"
|
||||
:reject-active="!configureBusy"
|
||||
reject-style="secondary"
|
||||
@confirm="onSubmitConfigure()"
|
||||
>
|
||||
<FormGroup>
|
||||
<div description v-html="$t('settings.updateScheduleDialog.description')"></div>
|
||||
|
||||
<p class="has-error text-center" v-show="configureError">{{ configureError }}</p>
|
||||
|
||||
<Radiobutton v-model="configureType" value="never" :label="$t('settings.updateScheduleDialog.disableCheckbox')" />
|
||||
<Radiobutton v-model="configureType" value="pattern" :label="$t('settings.updateScheduleDialog.enableCheckbox')"/>
|
||||
<label>{{ $t('settings.configureUpdates.policy') }}</label>
|
||||
<div>{{ $t('settings.configureUpdates.policyDescription') }}</div>
|
||||
<div style="padding-top: 10px">
|
||||
<Radiobutton v-model="configurePolicy" value="never" :label="$t('settings.updates.disabled')" />
|
||||
<Radiobutton v-model="configurePolicy" value="apps_only" :label="$t('settings.updates.appsOnly')" />
|
||||
<Radiobutton v-model="configurePolicy" value="platform_and_apps" :label="$t('settings.updates.platformAndApps')" />
|
||||
</div>
|
||||
</FormGroup>
|
||||
|
||||
<div v-show="configureType === 'pattern'" style="display: flex; gap: 10px; align-items: center; margin: 10px 0px 0px 25px">
|
||||
<div>{{ $t('settings.updateScheduleDialog.days') }}: <MultiSelect v-model="configureDays" :options="cronDays" option-label="name" option-key="id"/></div>
|
||||
<div>{{ $t('settings.updateScheduleDialog.hours') }}: <MultiSelect v-model="configureHours" :options="cronHours" option-label="name" option-key="id"/></div>
|
||||
<div class="text-small text-danger" v-show="configureType === 'pattern' && !(configureHours.length !== 0 && configureDays.length !== 0)">{{ $t('settings.updateScheduleDialog.selectOne') }}</div>
|
||||
<FormGroup>
|
||||
<div v-show="configurePolicy !== 'never'">
|
||||
<label>{{ $t('settings.configureUpdates.schedule') }}</label>
|
||||
<div style="display: flex; gap: 10px; align-items: center; margin-top: 12px">
|
||||
<div>{{ $t('settings.configureUpdates.days') }}: <MultiSelect v-model="configureDays" :options="cronDays" option-label="name" option-key="id"/></div>
|
||||
<div>{{ $t('settings.configureUpdates.hours') }}: <MultiSelect v-model="configureHours" :options="cronHours" option-label="name" option-key="id"/></div>
|
||||
<div class="text-small text-danger" v-show="!(configureHours.length !== 0 && configureDays.length !== 0)">{{ $t('settings.updateScheduleDialog.selectOne') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</Dialog>
|
||||
@@ -321,9 +332,10 @@ onMounted(async () => {
|
||||
|
||||
<SettingsItem v-if="ready">
|
||||
<div>
|
||||
<label>{{ $t('settings.updates.schedule') }}</label>
|
||||
<span v-if="currentPattern !== 'never'">{{ prettySchedule(currentPattern) }}</span>
|
||||
<span v-else>{{ $t('settings.updates.disabled') }}</span>
|
||||
<label>{{ $t('settings.updates.config') }}</label>
|
||||
<span v-if="currentPolicy === 'never'">{{ $t('settings.updates.disabled') }}</span>
|
||||
<span v-else-if="currentPolicy === 'apps_only'">{{ $t('settings.updates.appsOnly') }} - {{ prettySchedule(currentSchedule) }}</span>
|
||||
<span v-else>{{ $t('settings.updates.platformAndApps') }} - {{ prettySchedule(currentSchedule) }}</span>
|
||||
</div>
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button tool plain @click="onShowConfigure()">{{ $t('main.dialog.edit') }}</Button>
|
||||
|
||||
@@ -15,15 +15,12 @@ import SettingsItem from '../SettingsItem.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import BackupSitesModel from '../../models/BackupSitesModel.js';
|
||||
import BackupsModel from '../../models/BackupsModel.js';
|
||||
import TasksModel from '../../models/TasksModel.js';
|
||||
import { TASK_TYPES } from '../../constants.js';
|
||||
import BackupInfoDialog from '../BackupInfoDialog.vue';
|
||||
import ActionBar from '../../components/ActionBar.vue';
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const backupSitesModel = BackupSitesModel.create();
|
||||
const backupsModel = BackupsModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
|
||||
@@ -141,7 +138,7 @@ async function onChangeAutoBackups(value) {
|
||||
async function waitForTask() {
|
||||
if (!lastTask.value.id) return;
|
||||
|
||||
const [error, result] = await tasksModel.get(lastTask.value.id);
|
||||
const [error, result] = await appsModel.getAppTask(props.app.id, lastTask.value.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
lastTask.value = result;
|
||||
@@ -158,7 +155,7 @@ async function waitForTask() {
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
const [error, result] = await tasksModel.getByType(TASK_TYPES.TASK_APP_BACKUP_PREFIX + props.app.id);
|
||||
const [error, result] = await appsModel.listTasks(props.app.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
lastTask.value = result[0] || {};
|
||||
@@ -168,7 +165,7 @@ async function refreshTasks() {
|
||||
return {
|
||||
icon: 'fa-solid ' + ((!t.active && t.success) ? 'status-active fa-check-circle' : (t.active ? 'fa-circle-notch fa-spin' : 'status-error fa-times-circle')),
|
||||
label: prettyLongDate(t.ts),
|
||||
action: () => { window.open(`/logs.html?taskId=${t.id}`); }
|
||||
action: () => { window.open(`/logs.html?appId=${props.app.id}&taskId=${t.id}`); }
|
||||
};
|
||||
});
|
||||
|
||||
@@ -188,7 +185,7 @@ async function onStartBackup(backupSiteId) {
|
||||
async function onStopBackup() {
|
||||
stopBackupBusy.value = true;
|
||||
|
||||
const [error] = await tasksModel.stop(lastTask.value.id);
|
||||
const [error] = await appsModel.stopAppTask(props.app.id, lastTask.value.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
await refreshTasks();
|
||||
@@ -207,6 +204,7 @@ function onEdit(backup) {
|
||||
editLabel.value = backup.label || '';
|
||||
editError.value = '';
|
||||
editDialog.value.open();
|
||||
setTimeout(() => document.getElementById('labelInput').focus(), 500);
|
||||
}
|
||||
|
||||
async function onEditSubmit() {
|
||||
@@ -413,7 +411,7 @@ onUnmounted(() => {
|
||||
<div style="margin-top: 10px; display: flex; align-items: center; gap: 10px; overflow: hidden;">
|
||||
<div style="flex-grow: 1; overflow: hidden;">
|
||||
<ProgressBar :value="lastTask.percent" :show-label="false" :busy="true" :mode="lastTask.percent <= 0 ? 'indeterminate' : ''"/>
|
||||
<a :href="`/logs.html?taskId=${lastTask.id}`" target="_blank" style="display: block; margin-top: 3px; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">{{ lastTask.percent }}% {{ lastTask.message }}</a>
|
||||
<a :href="`/logs.html?appId=${props.app.id}&taskId=${lastTask.id}`" target="_blank" style="display: block; margin-top: 3px; max-width: 100%; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">{{ lastTask.percent }}% {{ lastTask.message }}</a>
|
||||
</div>
|
||||
<Button danger plain tool icon="fa-solid fa-xmark" @click="onStopBackup()" :loading="stopBackupBusy" :disabled="stopBackupBusy"></Button>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
const props = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Button, Radiobutton, InputGroup, FormGroup, TextInput, SingleSelect } from '@cloudron/pankow';
|
||||
@@ -55,6 +55,7 @@ async function onSendmailSubmit() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await props.refreshApp();
|
||||
sendmailBusy.value = false;
|
||||
}
|
||||
|
||||
@@ -78,6 +79,7 @@ async function onRecvmailSubmit() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await props.refreshApp();
|
||||
recvmailBusy.value = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import PortBindings from '../PortBindings.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import DomainsModel from '../../models/DomainsModel.js';
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
const props = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const domainsModel = DomainsModel.create();
|
||||
@@ -18,7 +18,7 @@ const busy = ref(false);
|
||||
const errorMessage = ref('');
|
||||
const errorObject = ref({});
|
||||
const overwriteDns = ref(false);
|
||||
const needsOverwriteDns = ref(false);
|
||||
const needsOverwriteDns = ref([]);
|
||||
const domain = ref('');
|
||||
const subdomain = ref('');
|
||||
const secondaryDomains = ref({});
|
||||
@@ -56,6 +56,12 @@ function onAddRedirect() {
|
||||
|
||||
const form = useTemplateRef('form');
|
||||
const isFormValid = ref(false);
|
||||
function resetDnsOverwrite() {
|
||||
needsOverwriteDns.value = [];
|
||||
overwriteDns.value = false;
|
||||
errorMessage.value = '';
|
||||
}
|
||||
|
||||
function checkValidity() {
|
||||
isFormValid.value = form.value ? form.value.checkValidity() : false;
|
||||
|
||||
@@ -87,7 +93,7 @@ async function onSubmit() {
|
||||
busy.value = true;
|
||||
errorMessage.value = '';
|
||||
errorObject.value = {};
|
||||
needsOverwriteDns.value = false;
|
||||
needsOverwriteDns.value = [];
|
||||
|
||||
const checkForDomains = [{
|
||||
domain: domain.value,
|
||||
@@ -98,6 +104,7 @@ async function onSubmit() {
|
||||
for (const d of aliases.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
|
||||
for (const d of redirects.value) checkForDomains.push({ domain: d.domain, subdomain: d.subdomain });
|
||||
|
||||
const conflicting = [];
|
||||
for (const d of checkForDomains) {
|
||||
const [error, result] = await domainsModel.checkRecords(d.domain, d.subdomain);
|
||||
if (error) {
|
||||
@@ -106,16 +113,19 @@ async function onSubmit() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
if (result.needsOverwrite && !overwriteDns.value) {
|
||||
busy.value = false;
|
||||
needsOverwriteDns.value = true;
|
||||
return;
|
||||
}
|
||||
if (result.needsOverwrite) conflicting.push((d.subdomain ? d.subdomain + '.' : '') + d.domain);
|
||||
}
|
||||
|
||||
if (conflicting.length > 0 && !overwriteDns.value) {
|
||||
busy.value = false;
|
||||
needsOverwriteDns.value = conflicting;
|
||||
errorMessage.value = `DNS records of ${conflicting.join(', ')} already exist`;
|
||||
return;
|
||||
}
|
||||
|
||||
// only use enabled ports
|
||||
const ports = {};
|
||||
const portsCombined = Object.assign(tcpPorts.value || {}, udpPorts.value || {});
|
||||
const portsCombined = Object.assign({}, tcpPorts.value || {}, udpPorts.value || {});
|
||||
for (const env in portsCombined) {
|
||||
if (portsCombined[env].enabled) {
|
||||
ports[env] = portsCombined[env].value;
|
||||
@@ -139,6 +149,7 @@ async function onSubmit() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await props.refreshApp();
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
@@ -206,8 +217,8 @@ onMounted(async () => {
|
||||
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<InputGroup style="flex-grow: 1">
|
||||
<TextInput style="flex-grow: 1" v-model="subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="domain" option-key="domain" option-label="label" :search-threshold="10" required/>
|
||||
<TextInput style="flex-grow: 1" v-model="subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10" required/>
|
||||
</InputGroup>
|
||||
<div class="warning-label" v-if="isNoopOrManual(domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((subdomain ? subdomain + '.' : '') + domain) })"></div>
|
||||
<!-- Button just to offset the same margin on the right to align location input when alias or redirects are visible -->
|
||||
@@ -219,8 +230,8 @@ onMounted(async () => {
|
||||
<label :for="'secondaryDomainInput' + item.containerPort">{{ item.title }}</label>
|
||||
<small>{{ item.description }}</small>
|
||||
<InputGroup style="flex-grow: 1">
|
||||
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + item.containerPort" v-model="item.subdomain" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10" required/>
|
||||
<TextInput style="flex-grow: 1" :id="'secondaryDomainInput' + item.containerPort" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('appstore.installDialog.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10" required/>
|
||||
</InputGroup>
|
||||
<div class="warning-label" v-if="isNoopOrManual(item.domain)" v-html="$t('appstore.installDialog.manualWarning', { location: ((item.subdomain ? item.subdomain + '.' : '') + item.domain) })"></div>
|
||||
</FormGroup>
|
||||
@@ -233,8 +244,8 @@ onMounted(async () => {
|
||||
<div v-for="(item, index) in aliases" :key="item" style="margin-bottom: 10px">
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<InputGroup style="flex-grow: 1">
|
||||
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10"/>
|
||||
<TextInput style="flex-grow: 1" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10"/>
|
||||
</InputGroup>
|
||||
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveAlias(index)"/>
|
||||
</div>
|
||||
@@ -252,8 +263,8 @@ onMounted(async () => {
|
||||
<div v-for="(item, index) in redirects" :key="item" style="margin-bottom: 10px;">
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<InputGroup style="flex-grow: 1">
|
||||
<TextInput style="flex-grow: 1" v-model="item.subdomain" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" :search-threshold="10"/>
|
||||
<TextInput style="flex-grow: 1" v-model="item.subdomain" @input="resetDnsOverwrite()" :placeholder="$t('app.location.locationPlaceholder')"/>
|
||||
<SingleSelect :disabled="busy" :options="domains" v-model="item.domain" option-key="domain" option-label="label" @select="resetDnsOverwrite()" :search-threshold="10"/>
|
||||
</InputGroup>
|
||||
<Button danger tool :disabled="busy" icon="fa-solid fa-trash" @click="onRemoveRedirect(index)"/>
|
||||
</div>
|
||||
@@ -271,13 +282,11 @@ onMounted(async () => {
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="error-label" v-if="errorMessage">{{ errorMessage }}</div>
|
||||
<br v-if="errorMessage"/>
|
||||
<div class="text-danger" v-if="errorMessage">{{ errorMessage }}</div>
|
||||
<Checkbox v-if="needsOverwriteDns.length" v-model="overwriteDns" :label="$t('app.location.overwriteDns', { domains: needsOverwriteDns.join(', ') })"/>
|
||||
<br v-if="needsOverwriteDns.length"/>
|
||||
|
||||
<Checkbox v-if="needsOverwriteDns" v-model="overwriteDns" :label="$t('app.location.dnsoverwrite')"/>
|
||||
<br v-if="needsOverwriteDns"/>
|
||||
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy || (app.error && app.error.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !isFormValid">{{ $t('app.location.saveAction') }}</Button>
|
||||
<Button @click="onSubmit()" :loading="busy" :disabled="busy || (app.error && app.error.installationState !== ISTATES.PENDING_LOCATION_CHANGE) || app.taskId || !isFormValid || (needsOverwriteDns.length > 0 && !overwriteDns)">{{ $t('app.location.saveAction') }}</Button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { taskNameFromInstallationState } from '../../utils.js';
|
||||
import { ISTATES } from '../../constants.js';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
const props = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const busyRepair = ref(false);
|
||||
@@ -28,8 +28,8 @@ async function onToggleDebugMode() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// let the task start
|
||||
setTimeout(() => { debugModeBusy.value = false; }, 4000);
|
||||
await props.refreshApp();
|
||||
debugModeBusy.value = false;
|
||||
}
|
||||
|
||||
async function onRepair() {
|
||||
@@ -42,7 +42,8 @@ async function onRepair() {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => { busyRepair.value = false; }, 4000);
|
||||
await props.refreshApp();
|
||||
busyRepair.value = false;
|
||||
}
|
||||
|
||||
async function onRestart() {
|
||||
|
||||
@@ -10,7 +10,7 @@ import SystemModel from '../../models/SystemModel.js';
|
||||
const appsModel = AppsModel.create();
|
||||
const systemModel = SystemModel.create();
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
const props = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
const memoryLimitBusy = ref(false);
|
||||
const memoryLimit = ref(0);
|
||||
@@ -33,8 +33,8 @@ async function onSubmitMemoryLimit() {
|
||||
const [error] = await appsModel.configure(props.app.id, 'memory_limit', { memoryLimit: limit });
|
||||
if (error) return console.error(error);
|
||||
|
||||
// give polling some time
|
||||
setTimeout(() => memoryLimitBusy.value = false, 4000);
|
||||
await props.refreshApp();
|
||||
memoryLimitBusy.value = false;
|
||||
}
|
||||
|
||||
async function onSubmitCpuQuota() {
|
||||
@@ -44,9 +44,8 @@ async function onSubmitCpuQuota() {
|
||||
if (error) return console.error(error);
|
||||
|
||||
currentCpuQuota.value = parseInt(cpuQuota.value);
|
||||
|
||||
// give polling some time
|
||||
setTimeout(() => cpuQuotaBusy.value = false, 4000);
|
||||
await props.refreshApp();
|
||||
cpuQuotaBusy.value = false;
|
||||
}
|
||||
|
||||
async function onSubmitDevices() {
|
||||
@@ -70,11 +69,9 @@ async function onSubmitDevices() {
|
||||
return;
|
||||
}
|
||||
|
||||
// give polling some time
|
||||
setTimeout(() => {
|
||||
devicesBusy.value = false;
|
||||
currentDevices.value = Object.keys(devs);
|
||||
}, 4000);
|
||||
currentDevices.value = Object.keys(devs);
|
||||
await props.refreshApp();
|
||||
devicesBusy.value = false;
|
||||
}
|
||||
|
||||
const devicesChanged = computed(() => {
|
||||
@@ -142,7 +139,7 @@ onMounted(async () => {
|
||||
<hr style="margin-top: 20px"/>
|
||||
|
||||
<form @submit.prevent="onSubmitDevices()" autocomplete="off">
|
||||
<fieldset :disabled="devicesBusy || (!app.error && !devicesChanged) || (app.error && app.error.installationState !== ISTATES.PENDING_RECREATE_CONTAINER) || app.taskId">
|
||||
<fieldset :disabled="devicesBusy || (app.error && app.error.installationState !== ISTATES.PENDING_RECREATE_CONTAINER) || app.taskId">
|
||||
<input style="display: none;" type="submit"/>
|
||||
<FormGroup>
|
||||
<label for="devicesInput">{{ $t('app.resources.devices.label') }} <sup><a href="https://docs.cloudron.io/apps/#devices" class="help" target="_blank"><i class="fa fa-question-circle"></i></a></sup></label>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { ISTATES } from '../../constants.js';
|
||||
import SettingsItem from '../SettingsItem.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
|
||||
const { app } = defineProps([ 'app' ]);
|
||||
const { app, refreshApp } = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
|
||||
@@ -24,6 +24,7 @@ async function onTurnChange(value) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await refreshApp();
|
||||
turnBusy.value = false;
|
||||
}
|
||||
|
||||
@@ -41,6 +42,7 @@ async function onRedisChange(value) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
await refreshApp();
|
||||
redisBusy.value = false;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { ISTATES } from '../../constants.js';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import VolumesModel from '../../models/VolumesModel.js';
|
||||
|
||||
const props = defineProps([ 'app' ]);
|
||||
const props = defineProps([ 'app', 'refreshApp' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const volumesModel = VolumesModel.create();
|
||||
@@ -56,9 +56,8 @@ async function onSubmitMove() {
|
||||
}
|
||||
|
||||
originalVolumeId.value = volumeId.value;
|
||||
|
||||
// give app refresh some time, ideally we wait for the task
|
||||
setTimeout(() => moveBusy.value = false, 4000);
|
||||
await props.refreshApp();
|
||||
moveBusy.value = false;
|
||||
}
|
||||
|
||||
function onMountAdd() {
|
||||
@@ -90,10 +89,9 @@ async function onSubmitMounts() {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// make a copy, cannot clone due to Proxy objects
|
||||
originalMounts.value = mounts.value.map(m => { return { volumeId: m.volumeId, readOnly: m.readOnly }; });
|
||||
|
||||
setTimeout(() => mountsBusy.value = false, 2000);
|
||||
await props.refreshApp();
|
||||
mountsBusy.value = false;
|
||||
}
|
||||
|
||||
const mountsValid = computed(() => {
|
||||
|
||||
@@ -26,11 +26,14 @@ async function onUninstall() {
|
||||
confirmLabel: t('app.uninstallDialog.uninstallAction'),
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
rejectStyle: 'secondary',
|
||||
autoCloseOnConfirm: false,
|
||||
});
|
||||
|
||||
if (!yes) return;
|
||||
|
||||
const [error] = await appsModel.uninstall(props.app.id);
|
||||
inputDialog.value.close();
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
window.location.href = '/#/apps';
|
||||
@@ -44,12 +47,15 @@ async function onArchive() {
|
||||
message: t('app.archiveDialog.description', { app: (props.app.label || props.app.fqdn), date: prettyLongDate(latestBackup.value.creationTime) }),
|
||||
confirmStyle: 'danger',
|
||||
confirmLabel: t('app.archive.action'),
|
||||
rejectLabel: t('main.dialog.cancel')
|
||||
rejectLabel: t('main.dialog.cancel'),
|
||||
autoCloseOnConfirm: false,
|
||||
});
|
||||
|
||||
if (!yes) return;
|
||||
|
||||
const [error] = await appsModel.archive(props.app.id, latestBackup.value.id);
|
||||
inputDialog.value.close();
|
||||
|
||||
if (error) return console.error(error);
|
||||
|
||||
window.location.href = '/#/apps';
|
||||
|
||||
@@ -7,13 +7,11 @@ import { ISTATES } from '../../constants.js';
|
||||
import SettingsItem from '../SettingsItem.vue';
|
||||
import AppsModel from '../../models/AppsModel.js';
|
||||
import ProfileModel from '../../models/ProfileModel.js';
|
||||
import TasksModel from '../../models/TasksModel.js';
|
||||
|
||||
const props = defineProps([ 'app', 'refresh-app' ]);
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const profileModel = ProfileModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
|
||||
const features = inject('features');
|
||||
|
||||
@@ -41,7 +39,7 @@ async function onAutoUpdatesEnabledChange(value) {
|
||||
async function waitForTask(id) {
|
||||
if (!id) return;
|
||||
|
||||
const [error, result] = await tasksModel.get(id);
|
||||
const [error, result] = await appsModel.getAppTask(props.app.id, id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
// task done, refresh menu
|
||||
|
||||
@@ -293,7 +293,6 @@ const STORAGE_PROVIDERS = [
|
||||
{ name: 'Cloudflare R2', value: 'cloudflare-r2' },
|
||||
{ name: 'Contabo Object Storage', value: 'contabo-objectstorage', regions: REGIONS_CONTABO },
|
||||
{ name: 'DigitalOcean Spaces', value: 'digitalocean-spaces', regions: REGIONS_DIGITALOCEAN },
|
||||
{ name: 'External/Local Disk (EXT4 or XFS)', value: 'disk' },
|
||||
{ name: 'EXT4 Disk', value: 'ext4' },
|
||||
{ name: 'Exoscale SOS', value: 'exoscale-sos', regions: REGIONS_EXOSCALE },
|
||||
{ name: 'Filesystem', value: 'filesystem' },
|
||||
|
||||
@@ -172,6 +172,39 @@ function create() {
|
||||
return {
|
||||
name: 'AppsModel',
|
||||
getTask,
|
||||
async listTasks(appId) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/tasks`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body.tasks];
|
||||
},
|
||||
async getAppTask(appId, taskId) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/tasks/${taskId}`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body];
|
||||
},
|
||||
async stopAppTask(appId, taskId) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${appId}/tasks/${taskId}/stop`, {}, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 204) return [error || result];
|
||||
return [null];
|
||||
},
|
||||
async install(appData, config) {
|
||||
const data = {
|
||||
subdomain: config.subdomain,
|
||||
|
||||
@@ -25,7 +25,7 @@ function ab2str(buf) {
|
||||
return String.fromCharCode.apply(null, new Uint16Array(buf));
|
||||
}
|
||||
|
||||
export function create(type, id) {
|
||||
export function create(type, id, options = {}) {
|
||||
const accessToken = localStorage.token;
|
||||
const INITIAL_STREAM_LINES = 100;
|
||||
|
||||
@@ -46,6 +46,9 @@ export function create(type, id) {
|
||||
} else if (type === 'service') {
|
||||
streamApi = `/api/v1/services/${id}/logstream`;
|
||||
downloadApi = `/api/v1/services/${id}/logs`;
|
||||
} else if (type === 'task' && options.appId) {
|
||||
streamApi = `/api/v1/apps/${options.appId}/tasks/${id}/logstream`;
|
||||
downloadApi = `/api/v1/apps/${options.appId}/tasks/${id}/logs`;
|
||||
} else if (type === 'task') {
|
||||
streamApi = `/api/v1/tasks/${id}/logstream`;
|
||||
downloadApi = `/api/v1/tasks/${id}/logs`;
|
||||
|
||||
@@ -234,17 +234,6 @@ function create() {
|
||||
if (error || result.status !== 201) return [error || result];
|
||||
return [null, result.body];
|
||||
},
|
||||
async getPasskey() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/profile/passkey`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body.passkey];
|
||||
},
|
||||
async getPasskeyRegistrationOptions() {
|
||||
let error, result;
|
||||
try {
|
||||
|
||||
@@ -17,10 +17,10 @@ function create() {
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body.update];
|
||||
},
|
||||
async getAutoupdatePattern() {
|
||||
async getAutoupdateConfig() {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/autoupdate_pattern`, { access_token: accessToken });
|
||||
result = await fetcher.get(`${API_ORIGIN}/api/v1/updater/autoupdate_config`, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
@@ -28,10 +28,10 @@ function create() {
|
||||
if (error || result.status !== 200) return [error || result];
|
||||
return [null, result.body];
|
||||
},
|
||||
async setAutoupdatePattern(pattern) {
|
||||
async setAutoupdateConfig(schedule, policy) {
|
||||
let error, result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/autoupdate_pattern`, { pattern }, { access_token: accessToken });
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/updater/autoupdate_config`, { schedule, policy }, { access_token: accessToken });
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
@@ -199,7 +199,7 @@ function create() {
|
||||
if (result.status !== 200) return [result];
|
||||
return [null, result.body.inviteLink];
|
||||
},
|
||||
async disableTwoFactorAuthentication(id) {
|
||||
async disableTotp(id) {
|
||||
let result;
|
||||
try {
|
||||
result = await fetcher.post(`${API_ORIGIN}/api/v1/users/${id}/totp_disable`, {}, { access_token: accessToken });
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import '@fontsource/inter';
|
||||
|
||||
import i18n from './i18n.js';
|
||||
import OidcDeviceConfirmView from './views/OidcDeviceConfirmView.vue';
|
||||
|
||||
import './style.css';
|
||||
|
||||
(async function init() {
|
||||
const app = createApp(OidcDeviceConfirmView);
|
||||
|
||||
app.use(await i18n());
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import '@fontsource/inter';
|
||||
|
||||
import i18n from './i18n.js';
|
||||
import OidcDeviceInputView from './views/OidcDeviceInputView.vue';
|
||||
|
||||
import './style.css';
|
||||
|
||||
(async function init() {
|
||||
const app = createApp(OidcDeviceInputView);
|
||||
|
||||
app.use(await i18n());
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
@@ -0,0 +1,16 @@
|
||||
import { createApp } from 'vue';
|
||||
|
||||
import '@fontsource/inter';
|
||||
|
||||
import i18n from './i18n.js';
|
||||
import OidcDeviceSuccessView from './views/OidcDeviceSuccessView.vue';
|
||||
|
||||
import './style.css';
|
||||
|
||||
(async function init() {
|
||||
const app = createApp(OidcDeviceSuccessView);
|
||||
|
||||
app.use(await i18n());
|
||||
|
||||
app.mount('#app');
|
||||
})();
|
||||
+35
-39
@@ -1,6 +1,6 @@
|
||||
|
||||
import { prettyBinarySize } from '@cloudron/pankow/utils';
|
||||
import { RELAY_PROVIDERS, ISTATES, STORAGE_PROVIDERS, EVENTS } from './constants.js';
|
||||
import { RELAY_PROVIDERS, ISTATES, EVENTS } from './constants.js';
|
||||
import { Marked } from 'marked';
|
||||
|
||||
function safeMarked() {
|
||||
@@ -43,7 +43,7 @@ function download(filename, text) {
|
||||
}
|
||||
|
||||
function mountlike(provider) {
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs' || provider === 'disk';
|
||||
return provider === 'sshfs' || provider === 'cifs' || provider === 'nfs' || provider === 'mountpoint' || provider === 'ext4' || provider === 'xfs';
|
||||
}
|
||||
|
||||
function s3like(provider) {
|
||||
@@ -55,39 +55,6 @@ function s3like(provider) {
|
||||
|| provider === 'contabo-objectstorage' || provider === 'synology-c2-objectstorage';
|
||||
}
|
||||
|
||||
function regionName(provider, endpoint) {
|
||||
const storageProvider = STORAGE_PROVIDERS.find(sp => sp.value === provider);
|
||||
const regions = storageProvider.regions;
|
||||
if (!regions) return endpoint;
|
||||
const region = regions.find(r => r.value === endpoint);
|
||||
if (!region) return endpoint;
|
||||
return region.name;
|
||||
}
|
||||
|
||||
function prettySiteLocation(site) {
|
||||
switch (site.provider) {
|
||||
case 'filesystem':
|
||||
return site.config.backupDir + (site.config.prefix ? `/${site.config.prefix}` : '');
|
||||
case 'disk':
|
||||
case 'ext4':
|
||||
case 'xfs':
|
||||
case 'mountpoint':
|
||||
return (site.config.mountOptions.diskPath || site.config.mountPoint) + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'cifs':
|
||||
case 'nfs':
|
||||
case 'sshfs':
|
||||
return site.config.mountOptions.host + ':' + site.config.mountOptions.remoteDir + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 's3':
|
||||
return site.config.region + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'minio':
|
||||
return site.config.endpoint + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
case 'gcs':
|
||||
return site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
default:
|
||||
return regionName(site.provider, site.config.endpoint) + ' / ' + site.config.bucket + (site.config.prefix ? ` / ${site.config.prefix}` : '');
|
||||
}
|
||||
}
|
||||
|
||||
function eventlogDetails(eventLog, app = null, appIdContext = '') {
|
||||
const data = eventLog.data;
|
||||
const errorMessage = data.errorMessage;
|
||||
@@ -693,6 +660,35 @@ function parseFullBackupPath(fullPath) {
|
||||
return { prefix, remotePath };
|
||||
}
|
||||
|
||||
function base64urlEncode(buffer) {
|
||||
return btoa(String.fromCharCode(...buffer))
|
||||
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
}
|
||||
|
||||
function generateCodeVerifier() {
|
||||
const array = new Uint8Array(32);
|
||||
crypto.getRandomValues(array);
|
||||
return base64urlEncode(array);
|
||||
}
|
||||
|
||||
async function computeCodeChallenge(verifier) {
|
||||
const hash = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
|
||||
return base64urlEncode(new Uint8Array(hash));
|
||||
}
|
||||
|
||||
async function startAuthFlow(clientId, apiOrigin) {
|
||||
const codeVerifier = generateCodeVerifier();
|
||||
const codeChallenge = await computeCodeChallenge(codeVerifier);
|
||||
|
||||
sessionStorage.setItem('pkce_code_verifier', codeVerifier);
|
||||
sessionStorage.setItem('pkce_client_id', clientId);
|
||||
sessionStorage.setItem('pkce_api_origin', apiOrigin || '');
|
||||
|
||||
const redirectUri = window.location.origin + '/authcallback.html';
|
||||
const base = apiOrigin || '';
|
||||
return `${base}/openid/auth?client_id=${clientId}&scope=openid email profile&response_type=code&redirect_uri=${redirectUri}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
}
|
||||
|
||||
// named exports
|
||||
export {
|
||||
renderSafeMarkdown,
|
||||
@@ -712,8 +708,8 @@ export {
|
||||
getColor,
|
||||
prettySchedule,
|
||||
parseSchedule,
|
||||
prettySiteLocation,
|
||||
parseFullBackupPath
|
||||
parseFullBackupPath,
|
||||
startAuthFlow
|
||||
};
|
||||
|
||||
// default export
|
||||
@@ -735,6 +731,6 @@ export default {
|
||||
getColor,
|
||||
prettySchedule,
|
||||
parseSchedule,
|
||||
prettySiteLocation,
|
||||
parseFullBackupPath
|
||||
parseFullBackupPath,
|
||||
startAuthFlow
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { ref, useTemplateRef, onMounted } from 'vue';
|
||||
import { Button, Checkbox, FormGroup, TextInput, PasswordInput, EmailInput } from '@cloudron/pankow';
|
||||
import ProvisionModel from '../models/ProvisionModel.js';
|
||||
import { redirectIfNeeded } from '../utils.js';
|
||||
import { redirectIfNeeded, startAuthFlow } from '../utils.js';
|
||||
|
||||
const provisionModel = ProvisionModel.create();
|
||||
|
||||
@@ -59,7 +59,7 @@ async function onOwnerSubmit() {
|
||||
// set token to autologin on first oidc flow
|
||||
localStorage.cloudronFirstTimeToken = result;
|
||||
|
||||
window.location.href = '/openid/auth?client_id=cid-webadmin&scope=openid email profile&response_type=code token&redirect_uri=' + window.location.origin + '/authcallback.html';
|
||||
window.location.href = await startAuthFlow('cid-webadmin', '');
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -26,11 +26,9 @@ import Storage from '../components/app/Storage.vue';
|
||||
import Uninstall from '../components/app/Uninstall.vue';
|
||||
import Updates from '../components/app/Updates.vue';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import { API_ORIGIN, APP_TYPES, ISTATES, RSTATES, HSTATES } from '../constants.js';
|
||||
|
||||
const appsModel = AppsModel.create();
|
||||
const tasksModel = TasksModel.create();
|
||||
const installationStateLabel = AppsModel.installationStateLabel;
|
||||
|
||||
const inputDialog = useTemplateRef('inputDialog');
|
||||
@@ -50,6 +48,8 @@ const sftpInfoDialog = useTemplateRef('sftpInfoDialog');
|
||||
|
||||
let refreshTimer = null;
|
||||
async function refresh() {
|
||||
clearTimeout(refreshTimer);
|
||||
|
||||
const [error, result] = await appsModel.get(id.value);
|
||||
if (error) {
|
||||
if (error.status === 403) return window.location.hash = '/';
|
||||
@@ -168,7 +168,7 @@ async function onStopAppTask() {
|
||||
|
||||
busyStopTask.value = true;
|
||||
|
||||
const [error] = await tasksModel.stop(app.value.taskId);
|
||||
const [error] = await appsModel.stopAppTask(app.value.id, app.value.taskId);
|
||||
if (error) console.error(error);
|
||||
|
||||
busyStopTask.value = false;
|
||||
@@ -342,19 +342,19 @@ onBeforeUnmount(() => {
|
||||
<Transition name="slide-fade" mode="out-in">
|
||||
<Info v-if="currentView === 'info'" :app="app"/>
|
||||
<Display v-else-if="currentView === 'display'" :app="app"/>
|
||||
<Location v-else-if="currentView === 'location'" :app="app"/>
|
||||
<Location v-else-if="currentView === 'location'" :app="app" :refresh-app="refresh"/>
|
||||
<Proxy v-else-if="currentView === 'proxy'" :app="app"/>
|
||||
<Access v-else-if="currentView === 'access'" :app="app"/>
|
||||
<Resources v-else-if="currentView === 'resources'" :app="app"/>
|
||||
<Services v-else-if="currentView === 'services'" :app="app"/>
|
||||
<Storage v-else-if="currentView === 'storage'" :app="app"/>
|
||||
<Resources v-else-if="currentView === 'resources'" :app="app" :refresh-app="refresh"/>
|
||||
<Services v-else-if="currentView === 'services'" :app="app" :refresh-app="refresh"/>
|
||||
<Storage v-else-if="currentView === 'storage'" :app="app" :refresh-app="refresh"/>
|
||||
<Graphs v-else-if="currentView === 'graphs'" :app="app"/>
|
||||
<Security v-else-if="currentView === 'security'" :app="app"/>
|
||||
<Email v-else-if="currentView === 'email'" :app="app"/>
|
||||
<Email v-else-if="currentView === 'email'" :app="app" :refresh-app="refresh"/>
|
||||
<Cron v-else-if="currentView === 'cron'" :app="app"/>
|
||||
<Updates v-else-if="currentView === 'updates'" :app="app" :refresh-app="refresh"/>
|
||||
<Backups v-else-if="currentView === 'backups'" :app="app"/>
|
||||
<Repair v-else-if="currentView === 'repair'" :app="app"/>
|
||||
<Repair v-else-if="currentView === 'repair'" :app="app" :refresh-app="refresh"/>
|
||||
<Eventlog v-else-if="currentView === 'eventlog'" :app="app"/>
|
||||
<Uninstall v-else-if="currentView === 'uninstall'" :app="app"/>
|
||||
</Transition>
|
||||
|
||||
@@ -171,7 +171,8 @@ function createAppLinkActionMenu(app) {
|
||||
const filteredApps = computed(() => {
|
||||
return apps.value.filter(a => {
|
||||
if (a.type === APP_TYPES.LINK) {
|
||||
return a.upstreamUri.includes(filter.value);
|
||||
return a.upstreamUri.includes(filter.value)
|
||||
|| (a.label ? a.label.toLowerCase().indexOf(filter.value.toLocaleLowerCase()) !== -1 : false);
|
||||
} else { // app or proxy
|
||||
return a.fqdn.includes(filter.value)
|
||||
|| a.secondaryDomains.some(sd => sd.fqdn.includes(filter.value))
|
||||
@@ -452,8 +453,8 @@ onDeactivated(() => {
|
||||
<div v-if="apps.length === 0" class="empty-placeholder">
|
||||
<!-- admins or not-->
|
||||
<div v-if="profile.isAtLeastAdmin">
|
||||
<h4>{{ $t('apps.noApps.title') }}</h4>
|
||||
<h5 v-html="$t('apps.noApps.description', { appStoreLink: '#/appstore' })"></h5>
|
||||
<h4 style="font-weight: 400">{{ $t('apps.noApps.title') }}</h4>
|
||||
<h5 v-html="$t('apps.noApps.description', { appStoreLink: '#/appstore' })" style="font-weight: 400"></h5>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h4>{{ $t('apps.noAccess.title') }}</h4>
|
||||
|
||||
@@ -20,7 +20,7 @@ import BackupSitesModel from '../models/BackupSitesModel.js';
|
||||
import TasksModel from '../models/TasksModel.js';
|
||||
import AppsModel from '../models/AppsModel.js';
|
||||
|
||||
import { prettySchedule, prettySiteLocation } from '../utils.js';
|
||||
import { prettySchedule } from '../utils.js';
|
||||
|
||||
const profile = inject('profile');
|
||||
|
||||
@@ -318,7 +318,7 @@ onMounted(async () => {
|
||||
|
||||
<div>
|
||||
<b>Storage:</b> {{ site.provider }} ({{ site.format }})
|
||||
<span>at {{ prettySiteLocation(site) }}</span>
|
||||
<span>at {{ site.locationLabel }}</span>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -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,7 +3,7 @@
|
||||
import { ref, onMounted, computed } from 'vue';
|
||||
import { prettyDecimalSize, sleep } from '@cloudron/pankow/utils';
|
||||
import { prettyRelayProviderName } from '../utils.js';
|
||||
import { TextInput } from '@cloudron/pankow';
|
||||
import { TextInput, ProgressBar } from '@cloudron/pankow';
|
||||
import Section from '../components/Section.vue';
|
||||
import StateLED from '../components/StateLED.vue';
|
||||
import DomainsModel from '../models/DomainsModel.js';
|
||||
@@ -13,6 +13,7 @@ const domainsModel = DomainsModel.create();
|
||||
const mailModel = MailModel.create();
|
||||
|
||||
const domains = ref([]);
|
||||
const busy = ref(true);
|
||||
|
||||
const searchFilter = ref('');
|
||||
|
||||
@@ -84,7 +85,10 @@ async function refreshUsage() {
|
||||
|
||||
onMounted(async () => {
|
||||
const [error, result] = await domainsModel.list();
|
||||
if (error) return console.error(error);
|
||||
if (error) {
|
||||
busy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
result.forEach(d => {
|
||||
d.loadingStatus = true;
|
||||
@@ -100,6 +104,7 @@ onMounted(async () => {
|
||||
});
|
||||
|
||||
domains.value = result;
|
||||
busy.value = false;
|
||||
|
||||
refreshStatus();
|
||||
refreshUsage();
|
||||
@@ -111,7 +116,7 @@ onMounted(async () => {
|
||||
<div class="content">
|
||||
<Section :title="$t('emails.domains.title')">
|
||||
<template #header-title-extra>
|
||||
<span style="font-weight: normal; font-size: 14px">({{ domains.length === 0 ? '-' : filteredDomains.length }})</span>
|
||||
<span style="font-weight: normal; font-size: 14px">({{ busy ? '-' : filteredDomains.length }})</span>
|
||||
</template>
|
||||
|
||||
<template #header-buttons>
|
||||
@@ -119,6 +124,7 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<div>
|
||||
<ProgressBar v-if="busy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
|
||||
<div v-if="domains.length !== 0 && filteredDomains.length === 0" class="email-placeholder">{{ $t('domains.noMatchesPlaceholder') }}</div>
|
||||
<a v-for="domain in filteredDomains" :key="domain.domain" :href="`#/email-domain/${domain.domain}`" class="email-domain">
|
||||
<div style="display: flex; align-items: center;">
|
||||
|
||||
@@ -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 }} <i v-if="group.source" class="far fa-address-book" v-tooltip="$t('users.groups.externalLdapTooltip')"></i>
|
||||
|
||||
@@ -16,6 +16,7 @@ const username = ref('');
|
||||
const password = ref('');
|
||||
const totpToken = ref('');
|
||||
const passkeyOptions = ref(null);
|
||||
const totpRequired = ref(false);
|
||||
const twoFARequired = ref(false);
|
||||
|
||||
// coming from oidc_login.html template
|
||||
@@ -24,6 +25,8 @@ const note = window.cloudron.note;
|
||||
const iconUrl = window.cloudron.iconUrl;
|
||||
const footer = window.cloudron.footer;
|
||||
const submitUrl = window.cloudron.submitUrl;
|
||||
const passkeyAuthOptionsUrl = window.cloudron.passkeyAuthOptionsUrl;
|
||||
const passkeyLoginUrl = window.cloudron.passkeyLoginUrl;
|
||||
|
||||
async function onSubmit() {
|
||||
busy.value = true;
|
||||
@@ -60,11 +63,11 @@ async function onSubmit() {
|
||||
if (res.body.twoFactorRequired) {
|
||||
twoFARequired.value = true;
|
||||
passkeyOptions.value = res.body.passkeyOptions || null;
|
||||
totpRequired.value = !!res.body.totpRequired;
|
||||
totpToken.value = '';
|
||||
|
||||
// If only passkeys are available (no TOTP), auto-trigger passkey flow
|
||||
if (passkeyOptions.value) await onUsePasskey();
|
||||
else setTimeout(() => document.getElementById('inputTotpToken')?.focus(), 0);
|
||||
else if (totpRequired.value) setTimeout(() => document.getElementById('inputTotpToken')?.focus(), 0);
|
||||
} else if (res.body.redirectTo) {
|
||||
return window.location.href = res.body.redirectTo;
|
||||
} else {
|
||||
@@ -122,6 +125,41 @@ async function onUsePasskey() {
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
async function onLoginWithPasskey() {
|
||||
busy.value = true;
|
||||
passkeyError.value = false;
|
||||
internalError.value = false;
|
||||
|
||||
try {
|
||||
const optionsRes = await fetcher.post(passkeyAuthOptionsUrl, {});
|
||||
if (optionsRes.status !== 200) {
|
||||
internalError.value = true;
|
||||
busy.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const credential = await startAuthentication({ optionsJSON: optionsRes.body });
|
||||
|
||||
const res = await fetcher.post(passkeyLoginUrl, { passkeyResponse: credential });
|
||||
|
||||
if (res.status === 200 && res.body.redirectTo) {
|
||||
window.location.href = res.body.redirectTo;
|
||||
return;
|
||||
} else if (res.status === 401) {
|
||||
passkeyError.value = true;
|
||||
} else {
|
||||
internalError.value = true;
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name !== 'NotAllowedError') {
|
||||
console.error('Passkey login failed', error);
|
||||
passkeyError.value = true;
|
||||
}
|
||||
}
|
||||
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// placed optionally in local storage by setupaccount.js
|
||||
const autoLoginToken = localStorage.cloudronFirstTimeToken;
|
||||
@@ -157,7 +195,10 @@ onMounted(async () => {
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<label for="inputPassword">{{ $t('login.password') }}</label>
|
||||
<div style="display: flex; justify-content: space-between; align-items: baseline;">
|
||||
<label for="inputPassword">{{ $t('login.password') }}</label>
|
||||
<a href="/passwordreset.html" tabindex="-1" style="font-size: 0.85em;">{{ $t('login.resetPasswordAction') }}</a>
|
||||
</div>
|
||||
<PasswordInput id="inputPassword" v-model="password" required/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -167,22 +208,22 @@ onMounted(async () => {
|
||||
</fieldset>
|
||||
|
||||
<div class="actions">
|
||||
<Button id="loginSubmitButton" @click="onSubmit" :disabled="busy || (!username || !password)" :loading="busy">{{ $t('login.loginAction') }}</Button>
|
||||
<a href="/passwordreset.html">{{ $t('login.resetPasswordAction') }}</a>
|
||||
<Button id="loginSubmitButton" @click="onSubmit" :disabled="busy || (!username || !password)">{{ $t('login.loginAction') }}</Button>
|
||||
<a href="#" @click.prevent="onLoginWithPasskey" tabindex="-1">{{ $t('login.passkeyAction') }}</a>
|
||||
</div>
|
||||
<div class="error-label" v-if="passkeyError">{{ $t('login.errorPasskeyFailed') }}</div>
|
||||
</form>
|
||||
|
||||
<form @submit.prevent="onSubmit" v-if="twoFARequired" autocomplete="off">
|
||||
<fieldset :disabled="busy">
|
||||
<input type="submit" style="display: none;"/>
|
||||
|
||||
<!-- Passkey or TOTP option -->
|
||||
<div v-if="passkeyOptions">
|
||||
<Button id="passkeyButton" @click="onUsePasskey" :disabled="busy" :loading="busy">{{ $t('login.usePasskeyAction') }}</Button>
|
||||
<Button id="passkeyButton" @click="onUsePasskey" :disabled="busy">{{ $t('login.usePasskeyAction') }}</Button>
|
||||
<div class="error-label" v-if="passkeyError">{{ $t('login.errorPasskeyFailed') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<div v-if="totpRequired">
|
||||
<FormGroup :has-error="totpError">
|
||||
<label for="inputTotpToken">{{ $t('login.2faToken') }}</label>
|
||||
<TextInput id="inputTotpToken" v-model="totpToken"/>
|
||||
@@ -191,7 +232,7 @@ onMounted(async () => {
|
||||
<div class="error-label" v-if="totpError">{{ $t('login.errorIncorrect2FAToken') }}</div>
|
||||
|
||||
<div class="actions">
|
||||
<Button id="totpTokenSubmitButton" @click="onSubmit" :disabled="busy || !totpToken" :loading="busy">{{ $t('login.loginAction') }}</Button>
|
||||
<Button id="totpTokenSubmitButton" @click="onSubmit" :disabled="busy || !totpToken">{{ $t('login.loginAction') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { marked } from 'marked';
|
||||
import { eachLimit } from 'async';
|
||||
import { ref, onMounted, inject, useTemplateRef } from 'vue';
|
||||
import { Button, ButtonGroup } from '@cloudron/pankow';
|
||||
import { Button, ButtonGroup, ProgressBar } from '@cloudron/pankow';
|
||||
import { prettyDate } from '@cloudron/pankow/utils';
|
||||
import Section from '../components/Section.vue';
|
||||
import NotificationSettingsDialog from '../components/NotificationSettingsDialog.vue';
|
||||
@@ -86,9 +86,12 @@ function onSetShowAll(value) {
|
||||
}
|
||||
|
||||
async function onLoadMore() {
|
||||
busy.value = true;
|
||||
if (!hasMore.value || busy.value) return;
|
||||
|
||||
const [error, result] = await notificationsModel.list(showAll.value ? null : false, page++);
|
||||
busy.value = true;
|
||||
page++;
|
||||
|
||||
const [error, result] = await notificationsModel.list(showAll.value ? null : false, page);
|
||||
if (error) return console.error(error);
|
||||
|
||||
hasMore.value = result.length >= PAGE_SIZE;
|
||||
@@ -97,6 +100,11 @@ async function onLoadMore() {
|
||||
busy.value = false;
|
||||
}
|
||||
|
||||
async function onScroll(event) {
|
||||
if (event.target.scrollTop + event.target.clientHeight >= event.target.scrollHeight) await onLoadMore();
|
||||
}
|
||||
|
||||
const notificationListContainer = useTemplateRef('notificationListContainer');
|
||||
const notificationSettingsDialog = useTemplateRef('notificationSettingsDialog');
|
||||
function onOpenSettings() {
|
||||
notificationSettingsDialog.value.open();
|
||||
@@ -111,10 +119,10 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content">
|
||||
<div class="content" style="height: 100%; overflow: hidden; display: flex; flex-direction: column;">
|
||||
<NotificationSettingsDialog ref="notificationSettingsDialog"/>
|
||||
|
||||
<Section :title="$t('notifications.title')">
|
||||
<Section :title="$t('notifications.title')" style="overflow: hidden; display: flex; flex-direction: column;">
|
||||
<template #filter-bar>
|
||||
<ButtonGroup>
|
||||
<Button :outline="showAll ? true : undefined" @click="onSetShowAll(false)">{{ $t('notifications.showUnread') }}</Button>
|
||||
@@ -126,7 +134,11 @@ onMounted(async () => {
|
||||
<Button @click="onOpenSettings" icon="fa fa-screwdriver-wrench" tool/>
|
||||
</template>
|
||||
|
||||
<div class="notification-list">
|
||||
<ProgressBar v-if="busy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
|
||||
|
||||
<div v-if="!busy && notifications.length === 0" class="empty-placeholder">{{ $t('notifications.allCaughtUp') }}</div>
|
||||
|
||||
<div ref="notificationListContainer" class="notification-list" @scroll="onScroll">
|
||||
<div v-for="notification in notifications" :key="notification.id" class="notification-item" :class="{ new: !notification.acknowledged, active: notification.active }">
|
||||
<div class="notification-item-title" @click="onToggleActive(notification)">
|
||||
<div>
|
||||
@@ -145,26 +157,28 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center; padding-bottom: 20px;">
|
||||
<Button @click="onLoadMore()" plain secondary v-if="hasMore" :disabled="busy" :loading="busy">{{ $t('main.action.loadMore') }}</Button>
|
||||
</div>
|
||||
</Section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.section-body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-direction: column;
|
||||
padding-bottom: 10px;
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
font-weight: var(--pankow-font-weight-bold);
|
||||
}
|
||||
|
||||
.notification-item.new {
|
||||
@@ -181,6 +195,7 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-weight: var(--pankow-font-weight-bold);
|
||||
}
|
||||
|
||||
.notification-item-date {
|
||||
@@ -191,7 +206,6 @@ onMounted(async () => {
|
||||
|
||||
.notification-item-message {
|
||||
display: none;
|
||||
font-size: 12px;
|
||||
padding-bottom: 10px;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
@@ -210,4 +224,9 @@ onMounted(async () => {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.empty-placeholder {
|
||||
text-align: center;
|
||||
margin-top: 60px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
|
||||
// keep this to load pankow Button css
|
||||
import { Button } from '@cloudron/pankow';
|
||||
|
||||
import PublicPageLayout from '../components/PublicPageLayout.vue';
|
||||
|
||||
// coming from oidc_device_confirm.html server-side rendered
|
||||
const clientName = window.cloudron.clientName;
|
||||
const userCode = window.cloudron.userCode;
|
||||
const form = window.cloudron.form;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PublicPageLayout>
|
||||
<div>
|
||||
<h2>Authorize {{ clientName }}</h2>
|
||||
<p>Verify the code below</p>
|
||||
<div class="user-code">{{ userCode }}</div>
|
||||
<p class="code-hint">If you did not initiate this action or the code does not match, please close this window or cancel.</p>
|
||||
|
||||
<!-- injected form for submission from oidcserver.js -->
|
||||
<div v-html="form"></div>
|
||||
|
||||
<button class="pankow-button" type="submit" form="op.deviceConfirmForm">Continue</button>
|
||||
<button type="submit" form="op.deviceConfirmForm" value="yes" name="abort" class="cancel-button">Cancel</button>
|
||||
</div>
|
||||
</PublicPageLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
p {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.user-code {
|
||||
font-size: 26px;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.code-hint {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
margin-left: 15px;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: var(--pankow-color-dark);
|
||||
}
|
||||
|
||||
.cancel-button:hover {
|
||||
color: var(--pankow-color-primary-hover);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
|
||||
import PublicPageLayout from '../components/PublicPageLayout.vue';
|
||||
|
||||
// coming from oidc_device_input.html server-side rendered
|
||||
const message = window.cloudron.message;
|
||||
const form = window.cloudron.form;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PublicPageLayout>
|
||||
<div>
|
||||
<h2>Authorization canceled</h2>
|
||||
|
||||
<!-- in theory one can enter the code manually there, but we don't support this right now
|
||||
<div v-html="message"></div>
|
||||
<div v-html="form"></div>
|
||||
<button type="submit" form="op.deviceInputForm">Continue</button>
|
||||
-->
|
||||
</div>
|
||||
</PublicPageLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
|
||||
import PublicPageLayout from '../components/PublicPageLayout.vue';
|
||||
|
||||
// coming from oidc_device_success.html server-side rendered (optional)
|
||||
const cloudronName = window.cloudron.name;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PublicPageLayout :cloudron-name="cloudronName">
|
||||
<div style="max-width: 300px;">
|
||||
<h2>Success</h2>
|
||||
<p>Your device has been authorized. You can close this window.</p>
|
||||
</div>
|
||||
</PublicPageLayout>
|
||||
</template>
|
||||
@@ -35,7 +35,7 @@ async function onPasswordReset() {
|
||||
if (res.status === 409) {
|
||||
error.value.generic = res.body.message;
|
||||
} else if (res.status === 202) {
|
||||
mode.value = MODE.NEW_PASSWORD_DONE;
|
||||
mode.value = MODE.RESET_PASSWORD_DONE;
|
||||
}
|
||||
} catch (error) {
|
||||
error.value.generic = error;
|
||||
@@ -46,6 +46,8 @@ async function onPasswordReset() {
|
||||
}
|
||||
|
||||
async function onNewPassword() {
|
||||
if (newPassword.value !== newPasswordRepeat.value) return;
|
||||
|
||||
busy.value = true;
|
||||
error.value = {};
|
||||
|
||||
|
||||
@@ -100,32 +100,23 @@ async function onRevokeAllWebAndCliTokens() {
|
||||
await profileModel.logout();
|
||||
}
|
||||
|
||||
const userPasskey = ref(null);
|
||||
const enableTwoFADialog = useTemplateRef('enableTwoFADialog');
|
||||
const has2FA = computed(() => profile.value.twoFactorAuthenticationEnabled || !!userPasskey.value);
|
||||
const has2FA = computed(() => profile.value.totpEnabled || profile.value.hasPasskey);
|
||||
|
||||
async function loadPasskey() {
|
||||
const [error, result] = await profileModel.getPasskey();
|
||||
if (error) return console.error('Failed to load passkey', error);
|
||||
userPasskey.value = result;
|
||||
}
|
||||
|
||||
async function onOpenTwoFASetupDialog() {
|
||||
enableTwoFADialog.value.open();
|
||||
async function onOpenTwoFASetupDialog(method) {
|
||||
enableTwoFADialog.value.open(method);
|
||||
}
|
||||
|
||||
async function onEnableTwoFASuccess() {
|
||||
await refreshProfile();
|
||||
await loadPasskey();
|
||||
}
|
||||
|
||||
async function onTwoFADisable() {
|
||||
disableTwoFADialog.value.open(userPasskey.value ? 'passkey' : 'totp');
|
||||
async function onTwoFADisable(method) {
|
||||
disableTwoFADialog.value.open(method);
|
||||
}
|
||||
|
||||
async function onTwoFADisableSuccess() {
|
||||
await refreshProfile();
|
||||
await loadPasskey();
|
||||
}
|
||||
|
||||
// Init
|
||||
@@ -161,10 +152,8 @@ onMounted(async () => {
|
||||
webadminTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_WEBADMIN || c.clientId === TOKEN_TYPES.ID_DEVELOPMENT || c.clientId === 'dashboard' || c.clientId === 'development'; });
|
||||
cliTokens.value = result.filter(function (c) { return c.clientId === TOKEN_TYPES.ID_CLI; });
|
||||
|
||||
await loadPasskey();
|
||||
|
||||
// check if we should show the 2fa setup
|
||||
if (config.value.mandatory2FA && !profile.value.twoFactorAuthenticationEnabled && !userPasskey.value) {
|
||||
if (config.value.mandatory2FA && !profile.value.totpEnabled && !profile.value.hasPasskey) {
|
||||
onOpenTwoFASetupDialog();
|
||||
}
|
||||
});
|
||||
@@ -230,13 +219,25 @@ onMounted(async () => {
|
||||
|
||||
<SettingsItem v-if="!profile.source || !config.external2FA">
|
||||
<FormGroup>
|
||||
<label>{{ $t('profile.twoFactorAuth.title') }}</label>
|
||||
<div v-if="!has2FA">{{ $t('profile.twoFactorAuth.disabled') }}</div>
|
||||
<div v-else-if="profile.twoFactorAuthenticationEnabled">{{ $t('profile.twoFactorAuth.totpEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
|
||||
<div v-else-if="userPasskey">{{ $t('profile.twoFactorAuth.passkeyEnabled') }} <i class="fa-solid fa-check text-success"></i></div>
|
||||
<label>{{ $t('profile.twoFactorAuth.totpTitle') }}</label>
|
||||
<div v-if="profile.totpEnabled">{{ $t('profile.twoFactorAuth.totpEnabled') }}</div>
|
||||
<div v-else-if="profile.id">{{ $t('profile.notSet') }}</div>
|
||||
</FormGroup>
|
||||
<div style="display: flex; align-items: center">
|
||||
<Button tool plain @click="has2FA ? onTwoFADisable() : onOpenTwoFASetupDialog()">{{ $t(has2FA ? 'profile.disable2FAAction' : 'profile.enable2FAAction') }}</Button>
|
||||
<div v-if="profile.id" style="display: flex; align-items: center">
|
||||
<Button tool plain v-if="profile.totpEnabled" @click="onTwoFADisable('totp')">{{ $t('main.action.disable') }}</Button>
|
||||
<Button tool plain v-else @click="onOpenTwoFASetupDialog('totp')">{{ $t('main.action.setup') }}</Button>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem v-if="!profile.source || !config.external2FA">
|
||||
<FormGroup>
|
||||
<label>{{ $t('profile.twoFactorAuth.passkeyTitle') }}</label>
|
||||
<div v-if="profile.hasPasskey">{{ $t('profile.twoFactorAuth.passkeyEnabled') }}</div>
|
||||
<div v-else-if="profile.id">{{ $t('profile.notSet') }}</div>
|
||||
</FormGroup>
|
||||
<div v-if="profile.id" style="display: flex; align-items: center">
|
||||
<Button tool plain v-if="profile.hasPasskey" @click="onTwoFADisable('passkey')">{{ $t('main.action.disable') }}</Button>
|
||||
<Button tool plain v-else @click="onOpenTwoFASetupDialog('passkey')">{{ $t('main.action.setup') }}</Button>
|
||||
</div>
|
||||
</SettingsItem>
|
||||
</div>
|
||||
|
||||
@@ -179,7 +179,7 @@ async function onSubmit() {
|
||||
config.mountOptions.privateKey = providerConfig.value.mountOptionPrivateKey;
|
||||
config.preserveAttributes = true;
|
||||
}
|
||||
} else if (provider.value === 'ext4' || provider.value === 'xfs' || provider.value === 'disk') {
|
||||
} else if (provider.value === 'ext4' || provider.value === 'xfs') {
|
||||
config.mountOptions.diskPath = providerConfig.value.mountOptionDiskPath;
|
||||
config.preserveAttributes = true;
|
||||
} else if (provider.value === 'mountpoint') {
|
||||
|
||||
@@ -18,6 +18,15 @@ import AppsModel from '../models/AppsModel.js';
|
||||
const appsModel = AppsModel.create();
|
||||
const servicesModel = ServicesModel.create();
|
||||
const systemModel = SystemModel.create();
|
||||
const BOX_SERVICE = Object.freeze({
|
||||
id: 'box',
|
||||
name: 'cloudron',
|
||||
status: 'active',
|
||||
memoryUsed: 0,
|
||||
memoryPercent: 0,
|
||||
memoryLimit: 0,
|
||||
config: {}
|
||||
});
|
||||
|
||||
const columns = {
|
||||
status: {},
|
||||
@@ -63,16 +72,7 @@ function createActionMenu(id) {
|
||||
}];
|
||||
}
|
||||
|
||||
const services = reactive({
|
||||
box: {
|
||||
name: 'cloudron',
|
||||
status: 'active',
|
||||
memoryUsed: 0,
|
||||
memoryUsage: '',
|
||||
memoryLimit: 0,
|
||||
config: {}
|
||||
}
|
||||
});
|
||||
const services = reactive({});
|
||||
|
||||
const servicesArray = computed(() => {
|
||||
return Object.keys(services).map(s => {
|
||||
@@ -101,24 +101,34 @@ async function refresh(id) {
|
||||
services[id].memoryPercent = result.memoryPercent || 0;
|
||||
services[id].defaultMemoryLimit = result.defaultMemoryLimit;
|
||||
|
||||
// we will poll until active
|
||||
if (result.status !== 'active' && !result.config.recoveryMode) servicesTimers[id] = setTimeout(refresh.bind(null, id), 3000);
|
||||
// we will poll until active (idle services are intentionally stopped, no need to poll)
|
||||
if (result.status !== 'active' && result.status !== 'idle' && !result.config.recoveryMode) servicesTimers[id] = setTimeout(refresh.bind(null, id), 3000);
|
||||
}
|
||||
|
||||
const refreshBusy = ref(false);
|
||||
const initialLoadBusy = ref(true);
|
||||
async function refreshAll() {
|
||||
refreshBusy.value = true;
|
||||
|
||||
let [error, result] = await appsModel.list();
|
||||
if (error) return console.error(error);
|
||||
if (error) {
|
||||
refreshBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
apps = result;
|
||||
|
||||
[error, result] = await servicesModel.list();
|
||||
if (error) return console.error(error);
|
||||
if (error) {
|
||||
refreshBusy.value = false;
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
const allServices = result;
|
||||
if (!services[BOX_SERVICE.id]) services[BOX_SERVICE.id] = { ...BOX_SERVICE, config: {} };
|
||||
|
||||
// init with all services
|
||||
for (const s of result.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
for (const s of allServices.sort((a, b) => a.name.localeCompare(b.name))) {
|
||||
if (!services[s.id]) services[s.id] = { id: s.id, name: s.name, config: {}, status: '' };
|
||||
|
||||
if (s.id.indexOf('redis') === 0) {
|
||||
@@ -128,7 +138,8 @@ async function refreshAll() {
|
||||
}
|
||||
}
|
||||
|
||||
await each(result.map(s => s.id), refresh);
|
||||
await each(allServices.map(s => s.id), refresh);
|
||||
initialLoadBusy.value = false;
|
||||
refreshBusy.value = false;
|
||||
}
|
||||
|
||||
@@ -196,8 +207,10 @@ async function onEditSubmit() {
|
||||
function state(service) {
|
||||
switch (service.status) {
|
||||
case 'active': return 'success';
|
||||
case 'idle': return 'idle';
|
||||
case 'disabled': return '';
|
||||
case 'stopped': return 'danger';
|
||||
case 'error': return 'danger';
|
||||
case 'starting': return service.config.recoveryMode ? '' : 'warning';
|
||||
default: return 'danger';
|
||||
}
|
||||
@@ -206,8 +219,10 @@ function state(service) {
|
||||
function stateTooltip(service) {
|
||||
switch (service.status) {
|
||||
case 'active': return 'Active';
|
||||
case 'idle': return 'Idle (starts on demand)';
|
||||
case 'disabled': return 'Disabled';
|
||||
case 'stopped': return 'Stopped';
|
||||
case 'error': return 'Error';
|
||||
case 'starting': return service.config.recoveryMode ? 'Recovery mode' : 'Starting';
|
||||
default: return service.status;
|
||||
}
|
||||
@@ -263,7 +278,9 @@ onUnmounted(() => {
|
||||
<div>{{ $t('services.description') }}</div>
|
||||
<br/>
|
||||
|
||||
<TableView :columns="columns" :model="servicesArray">
|
||||
<ProgressBar v-if="initialLoadBusy" mode="indeterminate" :show-label="false" :slim="true" :show-track="false"/>
|
||||
|
||||
<TableView v-if="!initialLoadBusy" :columns="columns" :model="servicesArray">
|
||||
<template #status="{ item:service }">
|
||||
<StateLED :busy="!service.status" :state="state(service)" :title="stateTooltip(service)"/>
|
||||
</template>
|
||||
|
||||
@@ -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';
|
||||
@@ -71,7 +71,7 @@ function createUserActionMenu(user) {
|
||||
}, {
|
||||
icon: 'fa-solid fa-qrcode',
|
||||
label: t('users.passwordResetDialog.reset2FAAction'),
|
||||
visible: user.twoFactorAuthenticationEnabled && !(user.source && external2FA),
|
||||
visible: user.totpEnabled && !(user.source && external2FA),
|
||||
disabled: !canEdit(user),
|
||||
action: on2FAReset.bind(null, user),
|
||||
}, {
|
||||
@@ -212,10 +212,10 @@ async function on2FAReset(user) {
|
||||
|
||||
if (!yes) return;
|
||||
|
||||
const [error] = await usersModel.disableTwoFactorAuthentication(user.id);
|
||||
const [error] = await usersModel.disableTotp(user.id);
|
||||
if (error) return console.error(error);
|
||||
|
||||
user.twoFactorAuthenticationEnabled = false;
|
||||
user.totpEnabled = false;
|
||||
}
|
||||
|
||||
function onEditOrAddUser(user = null) {
|
||||
@@ -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"/>
|
||||
|
||||
@@ -327,7 +327,7 @@ onMounted(async () =>{
|
||||
<TableView :columns="columns" :model="volumes" :busy="busy" :fixed-layout="true" :placeholder="$t('volumes.emptyPlaceholder')">
|
||||
<template #target="{ item:volume }">
|
||||
<span v-if="volume.mountType === 'mountpoint' || volume.mountType === 'filesystem'">{{ volume.hostPath }}</span>
|
||||
<span v-else-if="volume.mountType === 'ext4' || volume.mountType === 'xfs' || volume.mountType === 'disk'">{{ volume.mountOptions.diskPath }}</span>
|
||||
<span v-else-if="volume.mountType === 'ext4' || volume.mountType === 'xfs'">{{ volume.mountOptions.diskPath }}</span>
|
||||
<span v-else-if="volume.mountType === 'sshfs'">{{ volume.mountOptions.host + '/' + volume.mountOptions.remoteDir }}</span>
|
||||
<!-- cifs/nfs -->
|
||||
<span v-else>{{ volume.mountOptions.host + volume.mountOptions.remoteDir }}</span>
|
||||
|
||||
@@ -14,6 +14,9 @@ export default defineConfig({
|
||||
resolve('oidc_error.html'),
|
||||
resolve('oidc_interaction_confirm.html'),
|
||||
resolve('oidc_interaction_abort.html'),
|
||||
resolve('oidc_device_input.html'),
|
||||
resolve('oidc_device_confirm.html'),
|
||||
resolve('oidc_device_success.html'),
|
||||
resolve('logs.html'),
|
||||
resolve('passwordreset.html'),
|
||||
resolve('restore.html'),
|
||||
@@ -43,6 +46,9 @@ export default defineConfig({
|
||||
oidc_error: resolve('oidc_error.html'),
|
||||
oidc_interaction_confirm: resolve('oidc_interaction_confirm.html'),
|
||||
oidc_interaction_abort: resolve('oidc_interaction_abort.html'),
|
||||
oidc_device_input: resolve('oidc_device_input.html'),
|
||||
oidc_device_confirm: resolve('oidc_device_confirm.html'),
|
||||
oidc_device_success: resolve('oidc_device_success.html'),
|
||||
logs: resolve('logs.html'),
|
||||
notfound: resolve('notfound.html'),
|
||||
passwordreset: resolve('passwordreset.html'),
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var url = require('node:url');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var dbName = url.parse(process.env.DATABASE_URL).path.substr(1); // remove slash
|
||||
var dbName = new URL(process.env.DATABASE_URL).pathname.slice(1); // remove slash
|
||||
|
||||
// by default, mysql collates case insensitively. 'utf8_general_cs' is not available
|
||||
db.runSql('ALTER DATABASE ' + dbName + ' DEFAULT CHARACTER SET=utf8 DEFAULT COLLATE utf8_bin', callback);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
var safe = require('safetydance');
|
||||
var safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var tz = safe.fs.readFileSync('/etc/timezone', 'utf8');
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var url = require('node:url');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
var dbName = url.parse(process.env.DATABASE_URL).path.substr(1); // remove slash
|
||||
var dbName = new URL(process.env.DATABASE_URL).pathname.slice(1); // remove slash
|
||||
|
||||
// by default, mysql collates case insensitively. 'utf8_general_cs' is not available
|
||||
db.runSql('ALTER DATABASE ' + dbName + ' DEFAULT CHARACTER SET=utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci', callback);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
// first check precondtion of domain entry in settings
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
var async = require('async'),
|
||||
safe = require('safetydance'),
|
||||
safe = require('@cloudron/safetydance').default,
|
||||
tld = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -5,7 +5,7 @@ var async = require('async'),
|
||||
fs = require('node:fs'),
|
||||
os = require('node:os'),
|
||||
path = require('node:path'),
|
||||
safe = require('safetydance'),
|
||||
safe = require('@cloudron/safetydance').default,
|
||||
tldjs = require('tldjs');
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.runSql('ALTER TABLE domains ADD COLUMN wellKnownJson TEXT', function (error) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const async = require('async'),
|
||||
fs = require('node:fs'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
|
||||
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const async = require('async'),
|
||||
fs = require('node:fs'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const BOX_DATA_DIR = '/home/yellowtent/boxdata';
|
||||
const PLATFORM_DATA_DIR = '/home/yellowtent/platformdata';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const CERTS_DIR = '/home/yellowtent/boxdata/certs',
|
||||
PLATFORM_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const async = require('async'),
|
||||
fs = require('node:fs'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const CERTS_DIR = '/home/yellowtent/boxdata/certs';
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const async = require('async'),
|
||||
child_process = require('node:child_process'),
|
||||
fs = require('node:fs'),
|
||||
path = require('node:path'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const OLD_CERTS_DIR = '/home/yellowtent/boxdata/certs';
|
||||
const NEW_CERTS_DIR = '/home/yellowtent/platformdata/nginx/cert';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * FROM volumes', function (error, volumes) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const async = require('async'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = function(db, callback) {
|
||||
db.all('SELECT * from domains', [], function (error, results) {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const async = require('async'),
|
||||
openssl = require('../src/openssl.js').default,
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const NGINX_CERT_DIR = '/home/yellowtent/platformdata/nginx/cert';
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const async = require('async'),
|
||||
fs = require('node:fs'),
|
||||
path = require('node:path'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const MAIL_DATA_DIR = '/home/yellowtent/boxdata/mail';
|
||||
const DKIM_DIR = `${MAIL_DATA_DIR}/dkim`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const safe = require('safetydance');
|
||||
const safe = require('@cloudron/safetydance').default;
|
||||
|
||||
const PROXY_AUTH_TOKEN_SECRET_FILE = '/home/yellowtent/platformdata/proxy-auth-token-secret';
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const async = require('async'),
|
||||
mail = require('../src/mail.js').default,
|
||||
safe = require('safetydance'),
|
||||
safe = require('@cloudron/safetydance').default,
|
||||
util = require('node:util');
|
||||
|
||||
// it seems some mail domains do not have dkimKey in the database for some reason because of some previous bad migration
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
const crypto = require('node:crypto'),
|
||||
path = require('node:path'),
|
||||
safe = require('safetydance');
|
||||
safe = require('@cloudron/safetydance').default;
|
||||
|
||||
function getMountPoint(dataDir) {
|
||||
const output = safe.child_process.execSync(`df --output=target "${dataDir}" | tail -1`, { encoding: 'utf8' });
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const safe = require('safetydance');
|
||||
const safe = require('@cloudron/safetydance').default;
|
||||
|
||||
exports.up = async function (db) {
|
||||
const mailDomains = await db.runSql('SELECT * FROM mail', []);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user