mirror of
https://github.com/nextcloud/server.git
synced 2026-06-11 09:42:09 -04:00
feat: Implement settings frontend for allowed CORS domains
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
parent
e608e5d145
commit
33ae58e221
10 changed files with 395 additions and 10 deletions
|
|
@ -76,6 +76,8 @@ return [
|
|||
['name' => 'TwoFactorSettings#index', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'GET' , 'root' => ''],
|
||||
['name' => 'TwoFactorSettings#update', 'url' => '/settings/api/admin/twofactorauth', 'verb' => 'PUT' , 'root' => ''],
|
||||
['name' => 'AISettings#update', 'url' => '/settings/api/admin/ai', 'verb' => 'PUT' , 'root' => ''],
|
||||
['name' => 'CORSSettings#updateUserEnabled', 'url' => '/settings/api/admin/cors/allowusers', 'verb' => 'PUT' , 'root' => ''],
|
||||
['name' => 'CORSSettings#allowedDomains', 'url' => '/settings/api/admin/cors/domains', 'verb' => 'PUT' , 'root' => ''],
|
||||
|
||||
['name' => 'Help#help', 'url' => '/settings/help/{mode}', 'verb' => 'GET', 'defaults' => ['mode' => ''] , 'root' => ''],
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ return array(
|
|||
'OCA\\Settings\\Controller\\AppSettingsController' => $baseDir . '/../lib/Controller/AppSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\AuthSettingsController' => $baseDir . '/../lib/Controller/AuthSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\AuthorizedGroupController' => $baseDir . '/../lib/Controller/AuthorizedGroupController.php',
|
||||
'OCA\\Settings\\Controller\\CORSSettingsController' => $baseDir . '/../lib/Controller/CORSSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\ChangePasswordController' => $baseDir . '/../lib/Controller/ChangePasswordController.php',
|
||||
'OCA\\Settings\\Controller\\CheckSetupController' => $baseDir . '/../lib/Controller/CheckSetupController.php',
|
||||
'OCA\\Settings\\Controller\\CommonSettingsTrait' => $baseDir . '/../lib/Controller/CommonSettingsTrait.php',
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ class ComposerStaticInitSettings
|
|||
'OCA\\Settings\\Controller\\AppSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AppSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\AuthSettingsController' => __DIR__ . '/..' . '/../lib/Controller/AuthSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\AuthorizedGroupController' => __DIR__ . '/..' . '/../lib/Controller/AuthorizedGroupController.php',
|
||||
'OCA\\Settings\\Controller\\CORSSettingsController' => __DIR__ . '/..' . '/../lib/Controller/CORSSettingsController.php',
|
||||
'OCA\\Settings\\Controller\\ChangePasswordController' => __DIR__ . '/..' . '/../lib/Controller/ChangePasswordController.php',
|
||||
'OCA\\Settings\\Controller\\CheckSetupController' => __DIR__ . '/..' . '/../lib/Controller/CheckSetupController.php',
|
||||
'OCA\\Settings\\Controller\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Controller/CommonSettingsTrait.php',
|
||||
|
|
|
|||
88
apps/settings/lib/Controller/CORSSettingsController.php
Normal file
88
apps/settings/lib/Controller/CORSSettingsController.php
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 Ferdinand Thiessen <opensource@fthiessend.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License
|
||||
* as published by the Free Software Foundation,
|
||||
* either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*/
|
||||
namespace OCA\Settings\Controller;
|
||||
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
use OCP\Util;
|
||||
|
||||
class CORSSettingsController extends Controller {
|
||||
|
||||
/**
|
||||
* @param string $appName
|
||||
* @param IRequest $request
|
||||
* @param IConfig $config
|
||||
*/
|
||||
public function __construct(
|
||||
$appName,
|
||||
IRequest $request,
|
||||
private IConfig $config,
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether users can configure their own list of allowed CORS domains
|
||||
*
|
||||
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Security)
|
||||
*
|
||||
* @param bool $value
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function updateUserEnabled($value) {
|
||||
if (!is_bool($value)) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->config->setSystemValue('cors.allow-user-domains', $value);
|
||||
|
||||
return new DataResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set list of globally allowed CORS domains
|
||||
*
|
||||
* @AuthorizedAdminSetting(settings=OCA\Settings\Settings\Admin\Security)
|
||||
*
|
||||
* @param array $value
|
||||
* @return DataResponse
|
||||
*/
|
||||
public function allowedDomains(array $value) {
|
||||
try {
|
||||
foreach ($value as $entry) {
|
||||
if (!is_string($entry) || $entry === '' || Util::getFullDomain($entry) === '') {
|
||||
return new DataResponse([], HTTP::STATUS_BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return new DataResponse([], Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$this->config->setSystemValue('cors.allowed-domains', $value);
|
||||
|
||||
return new DataResponse();
|
||||
}
|
||||
}
|
||||
|
|
@ -30,6 +30,7 @@ use OC\Authentication\TwoFactorAuth\MandatoryTwoFactor;
|
|||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\ISettings;
|
||||
|
|
@ -40,17 +41,20 @@ class Security implements ISettings {
|
|||
private MandatoryTwoFactor $mandatoryTwoFactor;
|
||||
private IInitialState $initialState;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private IConfig $config;
|
||||
|
||||
public function __construct(IManager $manager,
|
||||
IUserManager $userManager,
|
||||
MandatoryTwoFactor $mandatoryTwoFactor,
|
||||
IInitialState $initialState,
|
||||
IURLGenerator $urlGenerator) {
|
||||
IURLGenerator $urlGenerator,
|
||||
IConfig $config) {
|
||||
$this->manager = $manager;
|
||||
$this->userManager = $userManager;
|
||||
$this->mandatoryTwoFactor = $mandatoryTwoFactor;
|
||||
$this->initialState = $initialState;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -76,6 +80,11 @@ class Security implements ISettings {
|
|||
$this->initialState->provideInitialState('encryption-modules', $encryptionModuleList);
|
||||
$this->initialState->provideInitialState('encryption-admin-doc', $this->urlGenerator->linkToDocs('admin-encryption'));
|
||||
|
||||
$this->initialState->provideInitialState('cors-allowed-domains', $this->config->getSystemValue('cors.allowed-domains', []));
|
||||
$this->initialState->provideInitialState('cors-allow-user-domains', $this->config->getSystemValue('cors.allow-user-domains', false));
|
||||
$this->initialState->provideInitialState('cors-settings-admin-docs', $this->urlGenerator->linkToDocs('admin-cors'));
|
||||
|
||||
|
||||
return new TemplateResponse('settings', 'settings/admin/security', [], '');
|
||||
}
|
||||
|
||||
|
|
|
|||
273
apps/settings/src/components/CORS.vue
Normal file
273
apps/settings/src/components/CORS.vue
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
<!--
|
||||
- @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
-
|
||||
- @license AGPL-3.0-or-later
|
||||
-
|
||||
- This code is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License
|
||||
- as published by the Free Software Foundation,
|
||||
- either version 3 of the License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<NcSettingsSection :name="t('settings', 'CORS allowed domains')"
|
||||
:description="t('settings', 'Cross-origin resource sharing (CORS) allows restricted resources (API) to be accessed from another external domain. The enabled domains will be allowed to access the DAV resources and CORS enabled API routes.')"
|
||||
:doc-url="corsSettingsAdminDoc">
|
||||
<NcCheckboxRadioSwitch :checked.sync="userCorsDomainsEnabled"
|
||||
type="switch"
|
||||
@update:checked="updateUserCorsDomains">
|
||||
{{ t('settings', 'Allow users to define a custom list of CORS enabled domains for their resources') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<section>
|
||||
<h3>{{ t('settings', 'CORS enabled external domains') }}</h3>
|
||||
<ul class="cors-domain__list">
|
||||
<li v-for="domain in allowedCorsDomains" :key="domain" class="cors-domain">
|
||||
<IconDomain :size="20" />
|
||||
<span class="cors-domain__text">{{ domain }}</span>
|
||||
<NcButton class="cors-domain__delete" type="tertiary" @click="removeCorsDomain(domain)">
|
||||
<template #icon>
|
||||
<IconTrashCan :size="20" />
|
||||
</template>
|
||||
Delete
|
||||
</NcButton>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<div class="cors-domain-input__wrapper">
|
||||
<NcTextField :error="inputError"
|
||||
:helper-text="inputErrorText"
|
||||
:label="t('settings', 'New CORS enabled domain')"
|
||||
:show-trailing-button="inputValue !== ''"
|
||||
:trailing-button-label="t('settings', 'Add CORS domain')"
|
||||
:value.sync="inputValue"
|
||||
class="cors-domain-input"
|
||||
placeholder="http://some.example.com"
|
||||
@keydown="onKeydownDomain"
|
||||
@trailing-button-click="inputValue = ''">
|
||||
<IconDomainPlus :size="20" />
|
||||
</NcTextField>
|
||||
<NcButton class="cors-domain-input__submit" @click="addCorsDomain(inputValue)">
|
||||
<template #icon>
|
||||
<IconCheck :size="20" />
|
||||
</template>
|
||||
{{ t('setting', 'Add domain') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import IconCheck from 'vue-material-design-icons/Check.vue'
|
||||
import IconDomain from 'vue-material-design-icons/Domain.vue'
|
||||
import IconDomainPlus from 'vue-material-design-icons/DomainPlus.vue'
|
||||
import IconTrashCan from 'vue-material-design-icons/TrashCan.vue'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
||||
import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { logger } from '../utils/logger.ts'
|
||||
|
||||
export default {
|
||||
name: 'CORS',
|
||||
components: {
|
||||
IconCheck,
|
||||
IconDomain,
|
||||
IconDomainPlus,
|
||||
IconTrashCan,
|
||||
NcButton,
|
||||
NcCheckboxRadioSwitch,
|
||||
NcSettingsSection,
|
||||
NcTextField,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
allowedCorsDomains: loadState('settings', 'cors-allowed-domains', []),
|
||||
userCorsDomainsEnabled: loadState('settings', 'cors-allow-user-domains', false),
|
||||
corsSettingsAdminDoc: loadState('settings', 'cors-settings-admin-docs', ''),
|
||||
inputValue: '',
|
||||
inputError: false,
|
||||
inputErrorText: '',
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
/**
|
||||
* Ensure errors are cleared on empty input or if the input is valid again
|
||||
*/
|
||||
inputValue() {
|
||||
if (this.inputValue === '' || (this.inputError && this.validateCorsDomain(this.inputValue))) {
|
||||
this.inputError = false
|
||||
this.inputErrorText = ''
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Add a new trusted CORS domain
|
||||
* @param {string} newDomain New domain to add as CORS enabled domain
|
||||
*/
|
||||
addCorsDomain(newDomain) {
|
||||
const domain = this.validateCorsDomain(newDomain)
|
||||
if (domain !== false) {
|
||||
const backup = [...this.allowedCorsDomains]
|
||||
this.allowedCorsDomains = [...this.allowedCorsDomains, domain]
|
||||
|
||||
this.update('domains', this.allowedCorsDomains).then(() => {
|
||||
this.inputValue = ''
|
||||
}).catch(() => {
|
||||
this.allowedCorsDomains = backup
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a domain from the list of CORS enabled domains
|
||||
* @param {string} domain Domain to remove from allowed domains
|
||||
*/
|
||||
removeCorsDomain(domain) {
|
||||
const backup = [...this.allowedCorsDomains]
|
||||
this.allowedCorsDomains = [...this.allowedCorsDomains.filter((entry) => entry !== domain)]
|
||||
this.update('domains', this.allowedCorsDomains).catch(() => {
|
||||
this.allowedCorsDomains = backup
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle Enter press on the CORS domain input field
|
||||
* @param {KeyboardEvent} event The keyboard event
|
||||
*/
|
||||
onKeydownDomain(event) {
|
||||
if (event.key === 'Enter') {
|
||||
this.addCorsDomain(this.inputValue)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Save user defined CORS domain
|
||||
* @param {boolean} checked Whether user defined lists are enabled
|
||||
*/
|
||||
updateUserCorsDomains(checked) {
|
||||
const backup = this.userCorsDomainsEnabled
|
||||
this.update('allowusers', checked).catch(() => {
|
||||
this.userCorsDomainsEnabled = backup
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Validate if a given string is a valid domain for the CORS headers
|
||||
* @param {string} domain A URL to validate
|
||||
* @return {string|false} Either the validated domain or false if invalid
|
||||
*/
|
||||
validateCorsDomain(domain) {
|
||||
try {
|
||||
const url = new URL(domain)
|
||||
if (url.hash !== '' || url.search !== '' || (url.pathname !== '' && url.pathname !== '/')) {
|
||||
this.inputError = true
|
||||
this.inputErrorText = t('settings', 'The domain must not contain any additional path or query parameters.')
|
||||
} else if (url.password !== '' || url.username !== '') {
|
||||
this.inputError = true
|
||||
this.inputErrorText = t('settings', 'The domain must not contain user and / or password.')
|
||||
} else if (url.origin === window.location.origin) {
|
||||
this.inputError = true
|
||||
this.inputErrorText = t('settings', 'The domain must not be the same like the current domain.')
|
||||
} else if (this.allowedCorsDomains.includes(url.origin)) {
|
||||
this.inputError = true
|
||||
this.inputErrorText = t('settings', 'The domain is already included.')
|
||||
} else {
|
||||
return url.origin
|
||||
}
|
||||
} catch (e) {
|
||||
this.inputError = true
|
||||
this.inputErrorText = t('settings', 'Invalid entered domain is not valid, please include protocol and hostname.')
|
||||
logger.debug('Invalid URL passed as CORS domain', { error: e })
|
||||
}
|
||||
return false
|
||||
},
|
||||
|
||||
async update(key, value) {
|
||||
await confirmPassword()
|
||||
|
||||
const url = generateUrl('/settings/api/admin/cors/{key}', {
|
||||
key,
|
||||
})
|
||||
|
||||
let reject = false
|
||||
try {
|
||||
const { status } = await axios.put(url, {
|
||||
value,
|
||||
})
|
||||
if (status !== 200) {
|
||||
showError(errorMessage)
|
||||
logger.error(errorMessage, { error })
|
||||
reject = true
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = t('settings', 'Unable to update CORS config')
|
||||
showError(errorMessage)
|
||||
logger.error(errorMessage, { error })
|
||||
}
|
||||
|
||||
if (reject) {
|
||||
throw new Error()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.cors-domain-input {
|
||||
margin-inline-start: 12px;
|
||||
|
||||
&__wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__submit {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cors-domain {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-large);
|
||||
padding: 4px;
|
||||
padding-inline-start: 12px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
&__list {
|
||||
margin-block: 6px 12px;
|
||||
margin-inline-start: 12px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-family: monospace;
|
||||
padding-inline: 24px 12px;
|
||||
}
|
||||
&__delete {
|
||||
margin-inline-start: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -80,18 +80,13 @@ import axios from '@nextcloud/axios'
|
|||
import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js'
|
||||
import NcButton from '@nextcloud/vue/dist/Components/NcButton.js'
|
||||
import NcSettingsSection from '@nextcloud/vue/dist/Components/NcSettingsSection.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||
import '@nextcloud/password-confirmation/dist/style.css'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
|
||||
const logger = getLoggerBuilder()
|
||||
.setApp('settings')
|
||||
.detectUser()
|
||||
.build()
|
||||
import { logger } from '../utils/logger.ts'
|
||||
|
||||
export default {
|
||||
name: 'Encryption',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Roeland Jago Douma <roeland@famdouma.nl>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license AGPL-3.0-or-later
|
||||
*
|
||||
|
|
@ -22,13 +23,16 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import Vue from 'vue'
|
||||
|
||||
import AdminTwoFactor from './components/AdminTwoFactor.vue'
|
||||
import Encryption from './components/Encryption.vue'
|
||||
import CORS from './components/CORS.vue'
|
||||
import store from './store/admin-security.js'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
import '@nextcloud/password-confirmation/dist/style.css'
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
__webpack_nonce__ = btoa(OC.requestToken)
|
||||
|
||||
|
|
@ -49,3 +53,6 @@ new View({
|
|||
|
||||
const EncryptionView = Vue.extend(Encryption)
|
||||
new EncryptionView().$mount('#vue-admin-encryption')
|
||||
|
||||
const CORSView = Vue.extend(CORS)
|
||||
new CORSView().$mount('#vue-admin-cors-settings')
|
||||
|
|
|
|||
6
apps/settings/src/utils/logger.ts
Normal file
6
apps/settings/src/utils/logger.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
export const logger = getLoggerBuilder()
|
||||
.setApp('settings')
|
||||
.detectUser()
|
||||
.build()
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
* @copyright 2022 Carl Schwan <carl@carlschwan.eu>
|
||||
*
|
||||
* @author Arthur Schiwon <blizzz@arthur-schiwon.de>
|
||||
* @author Ferdinand Thiessen <opensource@fthiessen.de>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
|
|
@ -28,3 +29,5 @@
|
|||
<div id="two-factor-auth-settings"></div>
|
||||
|
||||
<div id="vue-admin-encryption"></div>
|
||||
|
||||
<div id="vue-admin-cors-settings"></div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue