M5: notifications, setup check, audit log and password confirmation

- notifications to administrators for failed backup/restore runs and
  locations without a successful backup for two days (once a day)
- setup check in the admin overview: error when the key/kit is not set
  up, warning for missing, old or failed backups
- security events (recovery kit downloaded, location removed,
  append-only turned off, retention changed, snapshots forgotten) go to
  the audit log and are announced to all administrators, from the web
  and from occ; the scheduled weekly cleanup is logged only
- fix: the recovery-kit download link failed the CSRF check (412); it
  now requires a recent password confirmation instead, like creating the
  key, removing a location and restoring
This commit is contained in:
2026-10-02 02:29:46 +02:00
parent 38654317a2
commit b9e8858d76
18 changed files with 318 additions and 8 deletions
+18 -2
View File
@@ -23,7 +23,7 @@
<NcNoteCard type="warning">
{{ t('ng_backup', 'Before the first backup: download the recovery kit and store it, together with your passphrase, somewhere safe outside this server.') }}
</NcNoteCard>
<NcButton :href="kitUrl" download @click="downloaded = true">
<NcButton @click="downloadKit">
{{ t('ng_backup', 'Download recovery kit') }}
</NcButton>
<NcCheckboxRadioSwitch v-model="accepted" :disabled="!downloaded">
@@ -35,7 +35,8 @@
</div>
<p v-else>
{{ t('ng_backup', 'Recovery kit confirmed by {user} on {date}.', { user: status.key.confirmation.uid, date: new Date(status.key.confirmation.time * 1000).toLocaleString() }) }}
<a :href="kitUrl" download>{{ t('ng_backup', 'Download the kit again') }}</a>
<NcButton variant="tertiary" @click="downloadKit">{{ t('ng_backup', 'Download the kit again') }}</NcButton>
<span class="ngb-muted">{{ t('ng_backup', 'Requires your password; all administrators are notified.') }}</span>
</p>
</template>
<NcNoteCard v-if="error" type="error">{{ error }}</NcNoteCard>
@@ -50,6 +51,8 @@ import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwit
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { confirmPassword } from '@nextcloud/password-confirmation'
import '@nextcloud/password-confirmation/style.css'
import api from '../api.js'
const props = defineProps({ status: { type: Object, required: true } })
@@ -63,10 +66,22 @@ const busy = ref(false)
const error = ref('')
const kitUrl = api.kitUrl()
async function downloadKit() {
error.value = ''
try {
await confirmPassword()
window.location.href = kitUrl
downloaded.value = true
} catch (e) {
error.value = e?.message || ''
}
}
async function createKey() {
busy.value = true
error.value = ''
try {
await confirmPassword()
await api.initKey(pass1.value)
pass1.value = pass2.value = ''
emit('changed')
@@ -92,6 +107,7 @@ async function confirm() {
</script>
<style scoped>
.ngb-muted { color: var(--color-text-maxcontrast); margin-inline-start: 8px; }
.ngb-form, .ngb-confirm {
display: flex;
flex-direction: column;
+9 -1
View File
@@ -47,6 +47,8 @@ import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import NcTextField from '@nextcloud/vue/components/NcTextField'
import { confirmPassword } from '@nextcloud/password-confirmation'
import '@nextcloud/password-confirmation/style.css'
import api from '../api.js'
defineProps({ status: { type: Object, required: true } })
@@ -107,7 +109,13 @@ async function remove(target) {
if (!window.confirm(t('ng_backup', 'Remove "{name}" from NG Backup? The backups on the location itself are kept.', { name: target.name }))) {
return
}
await api.removeTarget(target.id)
try {
await confirmPassword()
await api.removeTarget(target.id)
} catch (e) {
error.value = e?.message || ''
return
}
emit('changed')
}
</script>
+3
View File
@@ -49,6 +49,8 @@ import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSelect from '@nextcloud/vue/components/NcSelect'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import { confirmPassword } from '@nextcloud/password-confirmation'
import '@nextcloud/password-confirmation/style.css'
import api from '../api.js'
import { formatSize } from '../format.js'
@@ -107,6 +109,7 @@ async function restore() {
busy.value = true
error.value = message.value = ''
try {
await confirmPassword()
await api.startRestore(target.value.id, snapshot.value.id, selected.value, mode.value)
message.value = t('ng_backup', 'Restore queued. It runs with the next cron job; progress is shown under Backups.')
emit('changed')