M4: admin settings page and JSON API

- AdminApiController (admin only, CSRF-protected): status, create key,
  download recovery kit, confirm (statement accepted + exact phrase),
  backends with their usable auth mechanisms, add/test/remove locations,
  start backups, schedule, snapshots, browse, queue restores
- RestoreJob runs web-initiated restores in the background; backup and
  restore runs are kept apart in the run table
- admin section 'NG Backup' with a Vue 3 / @nextcloud/vue 9 page: key and
  recovery-kit flow (download, tick statement, 'Yes, I confirm'), location
  form built from files_external's parameter definitions, schedule, run
  list with polling, snapshot browser and restore with the three modes
- auth mechanisms that depend on a user session or on files_external's
  own stored credentials are not offered

API tested with curl on NC34 (status, backends, schedule validation,
confirmation refusal, browse, queued restore and backup completed by
cron); non-admins get 403, anonymous 401.
This commit is contained in:
2026-10-02 01:09:47 +02:00
parent c601a02b2b
commit 36e6a3fde7
29 changed files with 9333 additions and 13 deletions
+62
View File
@@ -0,0 +1,62 @@
<!--
- SPDX-FileCopyrightText: 2026 Dillard Blom
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<template>
<div class="ng-backup">
<NcSettingsSection :name="t('ng_backup', 'NG Backup')"
:description="t('ng_backup', 'Encrypted, incremental backups of this Nextcloud (database, configuration and data). Backup locations are never mounted and are not visible in Files.')">
<NcLoadingIcon v-if="!status" :size="32" />
<NcNoteCard v-if="error" type="error">{{ error }}</NcNoteCard>
</NcSettingsSection>
<template v-if="status">
<KeySetup :status="status" @changed="refresh" />
<template v-if="status.key.confirmation">
<Locations :status="status" @changed="refresh" />
<Backups v-if="status.targets.length" :status="status" @changed="refresh" />
<RestoreBrowser v-if="status.targets.length" :status="status" @changed="refresh" />
</template>
</template>
</div>
</template>
<script setup>
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { t } from '@nextcloud/l10n'
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
import api from './api.js'
import Backups from './components/Backups.vue'
import KeySetup from './components/KeySetup.vue'
import Locations from './components/Locations.vue'
import RestoreBrowser from './components/RestoreBrowser.vue'
const status = ref(null)
const error = ref('')
let timer = null
async function refresh() {
try {
status.value = await api.status()
error.value = ''
} catch (e) {
error.value = e.message
}
// Poll while something runs, so progress shows up without reloading.
clearTimeout(timer)
if (status.value?.runs.some(r => r.status === 'running')) {
timer = setTimeout(refresh, 4000)
}
}
onMounted(refresh)
onBeforeUnmount(() => clearTimeout(timer))
</script>
<style scoped>
.ng-backup :deep(.settings-section) {
max-width: 900px;
}
</style>