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 ebd9737f51
commit 0309f2e44e
29 changed files with 9333 additions and 13 deletions
+18 -2
View File
@@ -26,16 +26,32 @@ final class ExternalStorageFactory {
}
/**
* @return list<array{id:string, name:string, auth:list<string>, parameters:array<string, mixed>}>
* @return list<array{id:string, name:string, auth:list<array{id:string, name:string, parameters:array}>, parameters:array<string, mixed>}>
* available backends, to build the settings form from
*/
public function describeBackends(): array {
$result = [];
foreach ($this->backends->getAvailableBackends() as $backend) {
$auth = [];
foreach ($this->backends->getAuthMechanismsByScheme(array_keys($backend->getAuthSchemes())) as $mech) {
// Mechanisms that need the logged-in user's session cannot work for unattended backups.
if (in_array($mech->getScheme(), ['builtin', 'null'], true) && $mech->getIdentifier() !== 'null::null') {
continue;
}
if (str_starts_with($mech->getIdentifier(), 'password::session') || str_starts_with($mech->getIdentifier(), 'password::logincredentials')
|| str_starts_with($mech->getIdentifier(), 'password::userprovided') || str_starts_with($mech->getIdentifier(), 'password::global')) {
continue;
}
$auth[] = [
'id' => $mech->getIdentifier(),
'name' => $mech->getText(),
'parameters' => array_map(fn ($p) => $p->jsonSerialize(), $mech->getParameters()),
];
}
$result[] = [
'id' => $backend->getIdentifier(),
'name' => $backend->getText(),
'auth' => array_keys($backend->getAuthSchemes()),
'auth' => $auth,
'parameters' => array_map(fn ($p) => $p->jsonSerialize(), $backend->getParameters()),
];
}