Skip to content

Commit f42898f

Browse files
committed
feat: add Vue 3 admin settings UI for backend configuration
Implements a Nextcloud admin settings page that lets administrators manage user_backends configuration through the web UI instead of editing config.php manually. AI-assisted: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Anna Larch <anna@nextcloud.com>
1 parent e4c5e88 commit f42898f

21 files changed

Lines changed: 8690 additions & 2 deletions

.github/workflows/node.yml

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# This workflow is provided via the organization template repository
2+
#
3+
# https://github.com/nextcloud/.github
4+
# https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization
5+
#
6+
# SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors
7+
# SPDX-License-Identifier: MIT
8+
9+
name: Node
10+
11+
on: pull_request
12+
13+
permissions:
14+
contents: read
15+
16+
concurrency:
17+
group: node-${{ github.head_ref || github.run_id }}
18+
cancel-in-progress: true
19+
20+
jobs:
21+
changes:
22+
runs-on: ubuntu-latest-low
23+
permissions:
24+
contents: read
25+
pull-requests: read
26+
27+
outputs:
28+
src: ${{ steps.changes.outputs.src}}
29+
30+
steps:
31+
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
32+
id: changes
33+
continue-on-error: true
34+
with:
35+
filters: |
36+
src:
37+
- '.github/workflows/**'
38+
- 'src/**'
39+
- 'appinfo/info.xml'
40+
- 'package.json'
41+
- 'package-lock.json'
42+
- 'tsconfig.json'
43+
- '**.js'
44+
- '**.ts'
45+
- '**.vue'
46+
47+
build:
48+
runs-on: ubuntu-latest
49+
50+
needs: changes
51+
if: needs.changes.outputs.src != 'false'
52+
53+
name: NPM build
54+
steps:
55+
- name: Checkout
56+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
57+
with:
58+
persist-credentials: false
59+
60+
- name: Read package.json node and npm engines version
61+
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
62+
id: versions
63+
with:
64+
fallbackNode: '^22'
65+
fallbackNpm: '^10'
66+
67+
- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
68+
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
69+
with:
70+
node-version: ${{ steps.versions.outputs.nodeVersion }}
71+
72+
- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
73+
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'
74+
75+
- name: Validate package-lock.json # See https://github.com/npm/cli/issues/4460
76+
run: |
77+
npm i -g npm-package-lock-add-resolved@1.1.4
78+
npm-package-lock-add-resolved
79+
git --no-pager diff --exit-code
80+
81+
- name: Install dependencies & build
82+
env:
83+
CYPRESS_INSTALL_BINARY: 0
84+
PUPPETEER_SKIP_DOWNLOAD: true
85+
run: |
86+
npm ci
87+
npm run build --if-present
88+
89+
- name: Check build changes
90+
run: |
91+
bash -c "[[ ! \"`git status --porcelain `\" ]] || (echo 'Please recompile and commit the assets, see the section \"Show changes on failure\" for details' && exit 1)"
92+
93+
- name: Show changes on failure
94+
if: failure()
95+
run: |
96+
git status
97+
git --no-pager diff
98+
exit 1 # make it red to grab attention
99+
100+
summary:
101+
permissions:
102+
contents: none
103+
runs-on: ubuntu-latest-low
104+
needs: [changes, build]
105+
106+
if: always()
107+
108+
# This is the summary, we just avoid to rename it so that branch protection rules still match
109+
name: node
110+
111+
steps:
112+
- name: Summary status
113+
run: if ${{ needs.changes.outputs.src != 'false' && needs.build.result != 'success' }}; then exit 1; fi

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,10 @@ vendor
1010
vendor-bin/*/vendor
1111
.php-cs-fixer.cache
1212
tests/.phpunit.cache
13+
14+
# frontend
15+
node_modules
16+
js/*.js
17+
js/*.js.map
18+
js/*.mjs.map
19+
js/*.LICENSE.txt

appinfo/info.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,8 @@ Read the [documentation](https://github.com/nextcloud/user_external#readme) to l
3535
<dependencies>
3636
<nextcloud min-version="31" max-version="32" />
3737
</dependencies>
38+
<settings>
39+
<admin>OCA\UserExternal\Settings\Admin</admin>
40+
<admin-section>OCA\UserExternal\Settings\AdminSection</admin-section>
41+
</settings>
3842
</info>

appinfo/routes.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
return [
6+
'routes' => [
7+
['name' => 'Config#getBackends', 'url' => '/api/v1/backends', 'verb' => 'GET'],
8+
['name' => 'Config#setBackends', 'url' => '/api/v1/backends', 'verb' => 'PUT'],
9+
],
10+
];

img/app.svg

Lines changed: 1 addition & 1 deletion
Loading

js/user_external-adminSettings.mjs

Lines changed: 26 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
SPDX-License-Identifier: (MPL-2.0 OR Apache-2.0)
2+
SPDX-License-Identifier: AGPL-3.0-or-later
3+
SPDX-License-Identifier: GPL-3.0-or-later
4+
SPDX-License-Identifier: ISC
5+
SPDX-License-Identifier: MIT
6+
SPDX-FileCopyrightText: @nextcloud/dialogs developers
7+
SPDX-FileCopyrightText: Christoph Wurst
8+
SPDX-FileCopyrightText: David Myers <hello@davidmyers.dev>
9+
SPDX-FileCopyrightText: Dr.-Ing. Mario Heiderich, Cure53 <mario@cure53.de> (https://cure53.de/)
10+
SPDX-FileCopyrightText: Eduardo San Martin Morote
11+
SPDX-FileCopyrightText: Evan You
12+
SPDX-FileCopyrightText: GitHub Inc.
13+
SPDX-FileCopyrightText: Guillaume Chau <guillaume.b.chau@gmail.com>
14+
SPDX-FileCopyrightText: Jeff Sagal <sagalbot@gmail.com>
15+
SPDX-FileCopyrightText: Matt Zabriskie
16+
SPDX-FileCopyrightText: Nextcloud GmbH and Nextcloud contributors
17+
SPDX-FileCopyrightText: Rob Cresswell <robcresswell@pm.me>
18+
SPDX-FileCopyrightText: Varun A P
19+
SPDX-FileCopyrightText: atomiks
20+
SPDX-FileCopyrightText: escape-html developers
21+
SPDX-FileCopyrightText: user_external developers
22+
23+
This file is generated from multiple sources. Included packages:
24+
- @floating-ui/core
25+
- version: 1.7.5
26+
- license: MIT
27+
- @floating-ui/dom
28+
- version: 1.1.1
29+
- license: MIT
30+
- @floating-ui/dom
31+
- version: 1.7.6
32+
- license: MIT
33+
- @floating-ui/utils
34+
- version: 0.2.11
35+
- license: MIT
36+
- @nextcloud/auth
37+
- version: 2.5.3
38+
- license: GPL-3.0-or-later
39+
- @nextcloud/axios
40+
- version: 2.5.2
41+
- license: GPL-3.0-or-later
42+
- @nextcloud/browser-storage
43+
- version: 0.5.0
44+
- license: GPL-3.0-or-later
45+
- @nextcloud/dialogs
46+
- version: 7.3.0
47+
- license: AGPL-3.0-or-later
48+
- @nextcloud/event-bus
49+
- version: 3.3.3
50+
- license: GPL-3.0-or-later
51+
- @nextcloud/initial-state
52+
- version: 3.0.0
53+
- license: GPL-3.0-or-later
54+
- @nextcloud/l10n
55+
- version: 3.4.1
56+
- license: GPL-3.0-or-later
57+
- @nextcloud/logger
58+
- version: 3.0.3
59+
- license: GPL-3.0-or-later
60+
- @nextcloud/router
61+
- version: 3.1.0
62+
- license: GPL-3.0-or-later
63+
- @nextcloud/vue
64+
- version: 9.6.0
65+
- license: AGPL-3.0-or-later
66+
- @vitejs/plugin-vue
67+
- version: 6.0.5
68+
- license: MIT
69+
- @vue/reactivity
70+
- version: 3.5.31
71+
- license: MIT
72+
- @vue/runtime-core
73+
- version: 3.5.31
74+
- license: MIT
75+
- @vue/runtime-dom
76+
- version: 3.5.31
77+
- license: MIT
78+
- @vue/shared
79+
- version: 3.5.31
80+
- license: MIT
81+
- axios
82+
- version: 1.13.6
83+
- license: MIT
84+
- dompurify
85+
- version: 3.3.3
86+
- license: (MPL-2.0 OR Apache-2.0)
87+
- escape-html
88+
- version: 1.0.3
89+
- license: MIT
90+
- floating-vue
91+
- version: 5.2.2
92+
- license: MIT
93+
- semver
94+
- version: 7.7.4
95+
- license: ISC
96+
- toastify-js
97+
- version: 1.12.0
98+
- license: MIT
99+
- user_external
100+
- version: 3.6.0
101+
- license: AGPL-3.0-or-later
102+
- vite
103+
- version: 7.3.1
104+
- license: MIT
105+
- vite-plugin-node-polyfills
106+
- version: 0.24.0
107+
- license: MIT
108+
- vue-material-design-icons
109+
- version: 5.3.1
110+
- license: MIT
111+
- vue-router
112+
- version: 5.0.4
113+
- license: MIT
114+
- vue-select
115+
- version: 4.0.0-beta.6
116+
- license: MIT

lib/AppInfo/Application.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
use OCP\AppFramework\Bootstrap\IRegistrationContext;
1111

1212
class Application extends App implements IBootstrap {
13+
public const APP_ID = 'user_external';
14+
1315
public function __construct() {
14-
parent::__construct('user_external');
16+
parent::__construct(self::APP_ID);
1517
}
1618

1719
public function register(IRegistrationContext $context): void {
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\UserExternal\Controller;
6+
7+
use OCA\UserExternal\Service\BackendConfigService;
8+
use OCP\AppFramework\Controller;
9+
use OCP\AppFramework\Http;
10+
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
11+
use OCP\AppFramework\Http\JSONResponse;
12+
use OCP\IRequest;
13+
14+
class ConfigController extends Controller {
15+
public function __construct(
16+
string $appName,
17+
IRequest $request,
18+
private readonly BackendConfigService $backendConfigService,
19+
) {
20+
parent::__construct($appName, $request);
21+
}
22+
23+
/**
24+
* Returns the current user_backends configuration.
25+
* Requires admin (no @NoAdminRequired).
26+
*/
27+
#[NoCSRFRequired]
28+
public function getBackends(): JSONResponse {
29+
return new JSONResponse($this->backendConfigService->getBackends());
30+
}
31+
32+
/**
33+
* Replaces the user_backends configuration.
34+
* Requires admin (no @NoAdminRequired).
35+
*
36+
* @param list<array{class: string, arguments: list<mixed>}> $backends
37+
*/
38+
public function setBackends(array $backends): JSONResponse {
39+
// Validate that each entry has the required keys and an allowed class.
40+
$allowed = [
41+
'\\OCA\\UserExternal\\IMAP',
42+
'\\OCA\\UserExternal\\FTP',
43+
'\\OCA\\UserExternal\\SMB',
44+
'\\OCA\\UserExternal\\SSH',
45+
'\\OCA\\UserExternal\\BasicAuth',
46+
'\\OCA\\UserExternal\\WebDavAuth',
47+
'\\OCA\\UserExternal\\XMPP',
48+
];
49+
50+
foreach ($backends as $backend) {
51+
if (!isset($backend['class'], $backend['arguments'])) {
52+
return new JSONResponse(['error' => 'Invalid backend format'], Http::STATUS_BAD_REQUEST);
53+
}
54+
if (!in_array($backend['class'], $allowed, true)) {
55+
return new JSONResponse(['error' => 'Unknown backend class: ' . $backend['class']], Http::STATUS_BAD_REQUEST);
56+
}
57+
}
58+
59+
$this->backendConfigService->setBackends($backends);
60+
return new JSONResponse(['status' => 'ok']);
61+
}
62+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace OCA\UserExternal\Service;
6+
7+
use OCP\IConfig;
8+
9+
/**
10+
* Reads and writes the user_backends system config key.
11+
*/
12+
class BackendConfigService {
13+
public function __construct(
14+
private readonly IConfig $config,
15+
) {
16+
}
17+
18+
/** @return list<array{class: string, arguments: list<mixed>}> */
19+
public function getBackends(): array {
20+
return $this->config->getSystemValue('user_backends', []);
21+
}
22+
23+
/** @param list<array{class: string, arguments: list<mixed>}> $backends */
24+
public function setBackends(array $backends): void {
25+
$this->config->setSystemValue('user_backends', $backends);
26+
}
27+
}

0 commit comments

Comments
 (0)