diff --git a/CHANGES b/CHANGES index d0d997f89..41b21fab1 100644 --- a/CHANGES +++ b/CHANGES @@ -3205,3 +3205,7 @@ * 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 update required + diff --git a/dashboard/public/translation/cs.json b/dashboard/public/translation/cs.json index 999b5e4af..8b4ef5e84 100644 --- a/dashboard/public/translation/cs.json +++ b/dashboard/public/translation/cs.json @@ -48,7 +48,9 @@ "configure": "Nakonfigurovat", "restart": "Restartovat", "reset": "Zresetovat", - "loadMore": "Načíst více" + "loadMore": "Načíst více", + "setup": "Nastavit", + "disable": "Zakázat" }, "rebootDialog": { "title": "Restart serveru", @@ -361,8 +363,23 @@ }, "twoFactorAuth": { "title": "Dvoufaktorová autentizace", - "totpEnabled": "Použít časově omezené jednorázové heslo (TOTP)", - "passkeyEnabled": "Použít passkey" + "totpEnabled": "Povoleno", + "passkeyEnabled": "Povoleno", + "totpTitle": "TOTP", + "passkeyTitle": "Passkey" + }, + "notSet": "nenastaveno", + "enablePasskey": { + "title": "Povolit passkey" + }, + "enableTotp": { + "title": "Povolit TOTP" + }, + "disableTotp": { + "title": "Zakázat TOTP" + }, + "disablePasskey": { + "title": "Zakázat passkey" } }, "backups": { @@ -648,7 +665,10 @@ "stopUpdateAction": "Zastavit aktualizaci", "disabled": "Zakázáno", "onLatest": "poslední", - "description": "Aktualizace platformy a aplikací se aplikují v nastavený čas podle časové zóny systému." + "description": "Aktualizace se aplikují v nastavený čas podle časové zóny systému.", + "config": "Automatické aktualizace", + "platformAndApps": "Platforma a aplikace", + "appsOnly": "Pouze aplikace" }, "updateScheduleDialog": { "disableCheckbox": "Zakázat automatické aktualizace", @@ -673,6 +693,14 @@ "registryConfig": { "provider": "Poskytovatel docker registrů", "providerOther": "Jiné" + }, + "configureUpdates": { + "title": "Konfigurovat automatické aktualizace", + "policy": "Politika", + "policyDescription": "Vyberte, co se bude automaticky aktualizovat", + "days": "Dnů/y", + "hours": "Hodin/y", + "schedule": "Plány záloh" } }, "branding": { @@ -1594,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", diff --git a/dashboard/public/translation/en.json b/dashboard/public/translation/en.json index 601c0af31..4745153e4 100644 --- a/dashboard/public/translation/en.json +++ b/dashboard/public/translation/en.json @@ -912,7 +912,8 @@ "rebootRequired": "Server reboot required", "cloudronUpdateFailed": "Cloudron update failed", "diskSpace": "Low disk space", - "appAutoUpdateFailed": "App automatic update failed" + "appAutoUpdateFailed": "App automatic update failed", + "manualUpdateRequired": "Platform or app requires manual update" }, "settingsDialog": { "description": "An email will be sent for the selected events to your primary email." diff --git a/dashboard/public/translation/id.json b/dashboard/public/translation/id.json index 4f4eca11f..912d7fdf8 100644 --- a/dashboard/public/translation/id.json +++ b/dashboard/public/translation/id.json @@ -44,7 +44,9 @@ "restart": "Mulai ulang", "reset": "Atur Ulang", "logs": "Log", - "loadMore": "Muat lebih banyak" + "loadMore": "Muat lebih banyak", + "setup": "Siapkan", + "disable": "Nonaktifkan" }, "searchPlaceholder": "Cari", "actions": "Tindakan", @@ -361,8 +363,23 @@ }, "twoFactorAuth": { "title": "Autentikasi dua faktor", - "totpEnabled": "Menggunakan kata sandi sekali pakai berbasis waktu (TOTP)", - "passkeyEnabled": "Menggunakan passkey" + "totpEnabled": "Diaktifkan", + "passkeyEnabled": "Diaktifkan", + "totpTitle": "TOTP", + "passkeyTitle": "Passkey" + }, + "notSet": "Belum diatur", + "enablePasskey": { + "title": "Aktifkan passkey" + }, + "enableTotp": { + "title": "Aktifkan TOTP" + }, + "disableTotp": { + "title": "Nonaktifkan TOTP" + }, + "disablePasskey": { + "title": "Nonaktifkan Passkey" } }, "backups": { @@ -724,8 +741,11 @@ "updateAvailableAction": "Pembaruan tersedia", "stopUpdateAction": "Hentikan pembaruan", "disabled": "Dinonaktifkan", - "description": "Pembaruan platform dan aplikasi diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan Zona waktu sistem.", - "onLatest": "terbaru" + "description": "Pembaruan diterapkan sesuai jadwal yang telah dikonfigurasi, menggunakan Zona waktu sistem.", + "onLatest": "terbaru", + "config": "Pembaruan otomatis", + "appsOnly": "Hanya aplikasi", + "platformAndApps": "Platform & aplikasi" }, "updateScheduleDialog": { "disableCheckbox": "Nonaktifkan pembaruan otomatis", @@ -750,6 +770,14 @@ "registryConfig": { "provider": "Penyedia registri Docker", "providerOther": "Lainnya" + }, + "configureUpdates": { + "title": "Konfigurasi Pembaruan Otomatis", + "policy": "Kebijakan", + "policyDescription": "Pilih apa yang diperbarui secara otomatis", + "days": "Hari", + "hours": "Jam", + "schedule": "Jadwal" } }, "support": { @@ -1662,7 +1690,8 @@ "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" }, "settingsDialog": { "description": "E-mail akan dikirimkan ke e-mail utama Anda untuk acara-acara yang dipilih." @@ -1688,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", diff --git a/dashboard/public/translation/nl.json b/dashboard/public/translation/nl.json index 791acd040..1112e973b 100644 --- a/dashboard/public/translation/nl.json +++ b/dashboard/public/translation/nl.json @@ -47,7 +47,9 @@ "configure": "Configureer", "restart": "Herstart", "reset": "Reset", - "loadMore": "Laad meer" + "loadMore": "Laad meer", + "setup": "Instellen", + "disable": "Uitschakelen" }, "rebootDialog": { "title": "Herstart Server", @@ -361,8 +363,23 @@ }, "twoFactorAuth": { "title": "Twee-Factor (2FA) authenticatie", - "totpEnabled": "Gebruikt tijdgebaseerd eenmalige wachtwoord (TOTP)", - "passkeyEnabled": "Gebruikt passkey" + "totpEnabled": "Ingeschakeld", + "passkeyEnabled": "Ingeschakeld", + "totpTitle": "TOTP", + "passkeyTitle": "Passkey" + }, + "notSet": "Niet ingesteld", + "enablePasskey": { + "title": "Passkey activeren" + }, + "enableTotp": { + "title": "TOTP activeren" + }, + "disableTotp": { + "title": "TOTP Uitschakelen" + }, + "disablePasskey": { + "title": "Passkey uitschakelen" } }, "backups": { @@ -1141,9 +1158,12 @@ "checkForUpdatesAction": "Controleer op updates", "updateAvailableAction": "Update beschikbaar", "stopUpdateAction": "Stop update", - "description": "Platform en app updates worden toegepast met de geconfigureerde planning met deze Systeem tijdzone.", + "description": "Updates worden toegepast volgens het geconfigureerde schema, met behulp van de System time zone.", "disabled": "Uitgeschakeld", - "onLatest": "Laatste" + "onLatest": "Laatste", + "config": "Automatische updates", + "appsOnly": "Alleen Apps", + "platformAndApps": "Platform & Apps" }, "updateScheduleDialog": { "disableCheckbox": "Automatische updates uitschakelen", @@ -1169,6 +1189,14 @@ "registryConfig": { "provider": "Docker registry aanbieder", "providerOther": "Anders" + }, + "configureUpdates": { + "title": "Automatische updates configureren", + "policy": "Beleid", + "policyDescription": "Kies wat er automatisch wordt bijgewerkt", + "days": "Dagen", + "hours": "Uren", + "schedule": "Planning" } }, "support": { @@ -1226,7 +1254,8 @@ "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" }, "settingsDialog": { "description": "Een e-mail wordt verstuurd voor de geselecteerde gebeurtenissen naar je primaire e-mail." @@ -1513,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", diff --git a/dashboard/src/components/NotificationSettingsDialog.vue b/dashboard/src/components/NotificationSettingsDialog.vue index a2eb4afd0..5c84e613e 100644 --- a/dashboard/src/components/NotificationSettingsDialog.vue +++ b/dashboard/src/components/NotificationSettingsDialog.vue @@ -17,6 +17,7 @@ const appAutoUpdateFailed = ref(false); const certificateRenewalFailed = ref(false); const diskSpace = ref(false); const cloudronUpdateFailed = ref(false); +const manualUpdateRequired = ref(false); const reboot = ref(false); async function onSubmit() { @@ -31,6 +32,7 @@ async function onSubmit() { if (certificateRenewalFailed.value) config.push('certificateRenewalFailed'); if (diskSpace.value) config.push('diskSpace'); if (cloudronUpdateFailed.value) config.push('cloudronUpdateFailed'); + if (manualUpdateRequired.value) config.push('manualUpdateRequired'); if (reboot.value) config.push('reboot'); const [error] = await profileModel.setNotificationConfig(config); @@ -55,6 +57,7 @@ async function open() { certificateRenewalFailed.value = config.indexOf('certificateRenewalFailed') !== -1; diskSpace.value = config.indexOf('diskSpace') !== -1; cloudronUpdateFailed.value = config.indexOf('cloudronUpdateFailed') !== -1; + manualUpdateRequired.value = config.indexOf('manualUpdateRequired') !== -1; reboot.value = config.indexOf('reboot') !== -1; dialogItem.value.open(); @@ -121,6 +124,11 @@ defineExpose({ + +
{{ $t('notifications.settings.manualUpdateRequired') }}
+ +
+
{{ $t('notifications.settings.rebootRequired') }}
diff --git a/src/mail_templates/box_manual_update_required-text.ejs b/src/mail_templates/box_manual_update_required-text.ejs new file mode 100644 index 000000000..75b24dc62 --- /dev/null +++ b/src/mail_templates/box_manual_update_required-text.ejs @@ -0,0 +1,14 @@ +Dear <%= cloudronName %> Admin, + +Cloudron v<%= version %> is available. + +Changelog: +<%- changelog %> + +Go to the Updates view to update: <%= updateUrl %> + +Powered by https://cloudron.io + +Don't want such mails? Change your notification preferences at <%= notificationsUrl %> + +Sent at: <%= new Date().toUTCString() %> diff --git a/src/mailer.js b/src/mailer.js index 56d7d98a2..f1877980c 100644 --- a/src/mailer.js +++ b/src/mailer.js @@ -322,6 +322,23 @@ async function boxUpdateError(mailTo, message) { await sendMail(mailOptions); } +async function boxManualUpdateRequired(mailTo, version, changelog) { + assert.strictEqual(typeof mailTo, 'string'); + assert.strictEqual(typeof version, 'string'); + assert.strictEqual(typeof changelog, 'string'); + + const mailConfig = await getMailConfig(); + + const mailOptions = { + from: mailConfig.notificationFrom, + to: mailTo, + subject: `[${mailConfig.cloudronName}] Cloudron v${version} is available`, + text: render('box_manual_update_required-text.ejs', { cloudronName: mailConfig.cloudronName, version, changelog, updateUrl: `https://${mailConfig.dashboardFqdn}/#/system-update`, notificationsUrl: mailConfig.notificationsUrl }) + }; + + await sendMail(mailOptions); +} + async function certificateRenewalError(mailTo, domain, message) { assert.strictEqual(typeof mailTo, 'string'); assert.strictEqual(typeof domain, 'string'); @@ -368,6 +385,7 @@ export default { appUp, oomEvent, rebootRequired, + boxManualUpdateRequired, boxUpdateError, lowDiskSpace, sendTestMail, diff --git a/src/notifications.js b/src/notifications.js index 81f5f68f0..790f6b8c0 100644 --- a/src/notifications.js +++ b/src/notifications.js @@ -29,6 +29,7 @@ const TYPE_UPDATE_UBUNTU = 'ubuntuUpdate'; const TYPE_BOX_UPDATE = 'boxUpdate'; const TYPE_MANUAL_APP_UPDATE_NEEDED = 'manualAppUpdate'; const TYPE_APP_AUTO_UPDATE_FAILED = 'appAutoUpdateFailed'; +const TYPE_MANUAL_UPDATE_REQUIRED = 'manualUpdateRequired'; const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'type', 'title', 'message', 'creationTime', 'acknowledged', 'context' ]; @@ -298,15 +299,30 @@ async function lowDiskSpace(message) { } } -async function onPin(type, message) { +async function boxManualUpdateRequired(version, changelogText) { + assert.strictEqual(typeof version, 'string'); + assert.strictEqual(typeof changelogText, 'string'); + + const admins = await users.getAdmins(); + for (const admin of admins) { + if (admin.notificationConfig.includes(TYPE_MANUAL_UPDATE_REQUIRED)) { + await safe(mailer.boxManualUpdateRequired(admin.email, version, changelogText), { debug: log }); + } + } +} + +async function onPin(type, message, options) { assert.strictEqual(typeof type, 'string'); // TYPE_ assert.strictEqual(typeof message, 'string'); + assert.strictEqual(typeof options, 'object'); switch (type) { case TYPE_REBOOT: return await rebootRequired(); case TYPE_DISK_SPACE: return await lowDiskSpace(message); + case TYPE_BOX_UPDATE: + return await boxManualUpdateRequired(options.context, message); default: break; } @@ -320,7 +336,7 @@ async function pin(type, title, message, options) { const result = await getByType(type, options.context || ''); if (!result) { - await onPin(type, message); + await onPin(type, message, options); return await add(type, title, message, { eventId: null, context: options.context || '' }); } @@ -328,7 +344,7 @@ async function pin(type, title, message, options) { const isUpdateType = type === TYPE_BOX_UPDATE || type === TYPE_MANUAL_APP_UPDATE_NEEDED; const acknowledged = (isUpdateType && result.message === message) ? result.acknowledged : false; - if (result.acknowledged && !acknowledged) await onPin(type, message); + if (result.acknowledged && !acknowledged) await onPin(type, message, options); await update(result, { title, message, acknowledged, creationTime: new Date() }); return result.id; @@ -412,6 +428,7 @@ export default { TYPE_BOX_UPDATE, TYPE_MANUAL_APP_UPDATE_NEEDED, TYPE_APP_AUTO_UPDATE_FAILED, + TYPE_MANUAL_UPDATE_REQUIRED, TYPE_DOMAIN_CONFIG_CHECK_FAILED, pin, unpin,