refactor(federation): migrate app frontend (admin settings) to Vue 3
Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
@@ -1,116 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param $ - The jQuery instance
|
||||
*/
|
||||
(function($) {
|
||||
// ocFederationAddServer
|
||||
$.fn.ocFederationAddServer = function() {
|
||||
/* Go easy on jquery and define some vars
|
||||
========================================================================== */
|
||||
|
||||
const $wrapper = $(this),
|
||||
|
||||
// Buttons
|
||||
$btnAddServer = $wrapper.find('#ocFederationAddServerButton'),
|
||||
$btnSubmit = $wrapper.find('#ocFederationSubmit'),
|
||||
|
||||
// Inputs
|
||||
$inpServerUrl = $wrapper.find('#serverUrl'),
|
||||
|
||||
// misc
|
||||
$msgBox = $wrapper.find('#ocFederationAddServer .msg'),
|
||||
$srvList = $wrapper.find('#listOfTrustedServers')
|
||||
|
||||
/* Interaction
|
||||
========================================================================== */
|
||||
|
||||
$btnAddServer.on('click', function() {
|
||||
$btnAddServer.addClass('hidden')
|
||||
$wrapper.find('.serverUrl').removeClass('hidden')
|
||||
$inpServerUrl
|
||||
.focus()
|
||||
})
|
||||
|
||||
// trigger server removal
|
||||
$srvList.on('click', 'li > .icon-delete', function() {
|
||||
const $this = $(this).parent()
|
||||
const id = $this.attr('id')
|
||||
|
||||
removeServer(id)
|
||||
})
|
||||
|
||||
$btnSubmit.on('click', function() {
|
||||
addServer($inpServerUrl.val())
|
||||
})
|
||||
|
||||
$inpServerUrl.on('change keyup', function(e) {
|
||||
const url = $(this).val()
|
||||
|
||||
// toggle add-button visibility based on input length
|
||||
if (url.length > 0) { $btnSubmit.removeClass('hidden') } else { $btnSubmit.addClass('hidden') }
|
||||
|
||||
if (e.keyCode === 13) { // add server on "enter"
|
||||
addServer(url)
|
||||
} else if (e.keyCode === 27) { // hide input filed again in ESC
|
||||
$btnAddServer.removeClass('hidden')
|
||||
$inpServerUrl.val('').addClass('hidden')
|
||||
$btnSubmit.addClass('hidden')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/* private Functions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
*/
|
||||
function addServer(url) {
|
||||
OC.msg.startSaving('#ocFederationAddServer .msg')
|
||||
|
||||
$.post(
|
||||
OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers',
|
||||
{
|
||||
url,
|
||||
},
|
||||
null,
|
||||
'json',
|
||||
).done(function({ ocs }) {
|
||||
const data = ocs.data
|
||||
$('#serverUrl').attr('value', '')
|
||||
$('#listOfTrustedServers').prepend($('<li>')
|
||||
.attr('id', data.id)
|
||||
.html('<span class="status indeterminate"></span>'
|
||||
+ data.url
|
||||
+ '<span class="icon icon-delete"></span>'))
|
||||
OC.msg.finishedSuccess('#ocFederationAddServer .msg', data.message)
|
||||
})
|
||||
.fail(function(jqXHR) {
|
||||
OC.msg.finishedError('#ocFederationAddServer .msg', JSON.parse(jqXHR.responseText).ocs.meta.message)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
function removeServer(id) {
|
||||
$.ajax({
|
||||
url: OC.getRootPath() + '/ocs/v2.php/apps/federation/trusted-servers/' + id,
|
||||
type: 'DELETE',
|
||||
success: function(response) {
|
||||
$('#ocFederationSettings').find('#' + id).remove()
|
||||
},
|
||||
})
|
||||
}
|
||||
})(jQuery)
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
$('#ocFederationSettings').ocFederationAddServer()
|
||||
})
|
||||
@@ -18,11 +18,13 @@ use OCP\Federation\Events\TrustedServerRemovedEvent;
|
||||
|
||||
class Application extends App implements IBootstrap {
|
||||
|
||||
public const APP_ID = 'federation';
|
||||
|
||||
/**
|
||||
* @param array $urlParams
|
||||
*/
|
||||
public function __construct($urlParams = []) {
|
||||
parent::__construct('federation', $urlParams);
|
||||
parent::__construct(self::APP_ID, $urlParams);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
namespace OCA\Federation\Settings;
|
||||
|
||||
use OCA\Federation\AppInfo\Application;
|
||||
use OCA\Federation\TrustedServers;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\IDelegatedSettings;
|
||||
use OCP\Util;
|
||||
|
||||
class Admin implements IDelegatedSettings {
|
||||
public function __construct(
|
||||
private TrustedServers $trustedServers,
|
||||
private IInitialState $initialState,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private IL10N $l,
|
||||
) {
|
||||
}
|
||||
@@ -24,9 +30,14 @@ class Admin implements IDelegatedSettings {
|
||||
public function getForm() {
|
||||
$parameters = [
|
||||
'trustedServers' => $this->trustedServers->getServers(),
|
||||
'docUrl' => $this->urlGenerator->linkToDocs('admin-sharing-federated') . '#configuring-trusted-nextcloud-servers',
|
||||
];
|
||||
|
||||
return new TemplateResponse('federation', 'settings-admin', $parameters, '');
|
||||
$this->initialState->provideInitialState('adminSettings', $parameters);
|
||||
|
||||
Util::addStyle(Application::APP_ID, 'settings-admin');
|
||||
Util::addScript(Application::APP_ID, 'settings-admin');
|
||||
return new TemplateResponse(Application::APP_ID, 'settings-admin', renderAs: '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<!--
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ITrustedServer } from '../services/api.ts'
|
||||
|
||||
import { mdiPlus } from '@mdi/js'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { nextTick, ref, useTemplateRef } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import { addServer, ApiError } from '../services/api.ts'
|
||||
import { logger } from '../services/logger.ts'
|
||||
|
||||
const emit = defineEmits<{
|
||||
add: [server: ITrustedServer]
|
||||
}>()
|
||||
|
||||
const formElement = useTemplateRef<HTMLFormElement>('form')
|
||||
const newServerUrl = ref('')
|
||||
|
||||
/**
|
||||
* Handle add trusted server form submission
|
||||
*/
|
||||
async function onAdd() {
|
||||
try {
|
||||
const server = await addServer(newServerUrl.value)
|
||||
newServerUrl.value = ''
|
||||
emit('add', server)
|
||||
|
||||
nextTick(() => formElement.value?.reset()) // Reset native form validation state
|
||||
showSuccess(t('federation', 'Added to the list of trusted servers'))
|
||||
} catch (error) {
|
||||
logger.error('Failed to add trusted server', { error })
|
||||
if (error instanceof ApiError) {
|
||||
showError(error.message)
|
||||
} else {
|
||||
showError(t('federation', 'Could not add trusted server. Please try again later.'))
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form ref="form" @submit.prevent="onAdd">
|
||||
<h3 :class="$style.addTrustedServerForm__heading">
|
||||
{{ t('federation', 'Add trusted server') }}
|
||||
</h3>
|
||||
<div :class="$style.addTrustedServerForm__wrapper">
|
||||
<NcTextField
|
||||
v-model="newServerUrl"
|
||||
:label="t('federation', 'Server url')"
|
||||
placeholder="https://…"
|
||||
required
|
||||
type="url" />
|
||||
<NcButton
|
||||
:class="$style.addTrustedServerForm__submitButton"
|
||||
:aria-label="t('federation', 'Add')"
|
||||
:title="t('federation', 'Add')"
|
||||
type="submit"
|
||||
variant="primary">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiPlus" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.addTrustedServerForm__heading {
|
||||
font-size: 1.2rem;
|
||||
margin-block: 0.5lh 0.25lh;
|
||||
}
|
||||
|
||||
.addTrustedServerForm__wrapper {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
align-items: end;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.addTrustedServerForm__submitButton {
|
||||
max-height: var(--default-clickable-area);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,121 @@
|
||||
<!--
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ITrustedServer } from '../services/api.ts'
|
||||
|
||||
import { mdiCheckNetworkOutline, mdiCloseNetworkOutline, mdiHelpNetworkOutline, mdiTrashCanOutline } from '@mdi/js'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon'
|
||||
import { TrustedServerStatus } from '../services/api.ts'
|
||||
import { deleteServer } from '../services/api.ts'
|
||||
import { logger } from '../services/logger.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
server: ITrustedServer
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
delete: [ITrustedServer]
|
||||
}>()
|
||||
|
||||
const isLoading = ref(false)
|
||||
|
||||
const hasError = computed(() => props.server.status === TrustedServerStatus.STATUS_FAILURE)
|
||||
const serverIcon = computed(() => {
|
||||
switch (props.server.status) {
|
||||
case TrustedServerStatus.STATUS_OK:
|
||||
return mdiCheckNetworkOutline
|
||||
case TrustedServerStatus.STATUS_PENDING:
|
||||
case TrustedServerStatus.STATUS_ACCESS_REVOKED:
|
||||
return mdiHelpNetworkOutline
|
||||
case TrustedServerStatus.STATUS_FAILURE:
|
||||
default:
|
||||
return mdiCloseNetworkOutline
|
||||
}
|
||||
})
|
||||
|
||||
const serverStatus = computed(() => {
|
||||
switch (props.server.status) {
|
||||
case TrustedServerStatus.STATUS_OK:
|
||||
return [t('federation', 'Server ok'), t('federation', 'User list was exchanged at least once successfully with the remote server.')]
|
||||
case TrustedServerStatus.STATUS_PENDING:
|
||||
return [t('federation', 'Server pending'), t('federation', 'Waiting for shared secret or initial user list exchange.')]
|
||||
case TrustedServerStatus.STATUS_ACCESS_REVOKED:
|
||||
return [t('federation', 'Server access revoked'), t('federation', 'Server access revoked')]
|
||||
case TrustedServerStatus.STATUS_FAILURE:
|
||||
default:
|
||||
return [t('federation', 'Server failure'), t('federation', 'Connection to the remote server failed or the remote server is misconfigured.')]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Emit delete event
|
||||
*/
|
||||
async function onDelete() {
|
||||
try {
|
||||
isLoading.value = true
|
||||
await deleteServer(props.server.id)
|
||||
emit('delete', props.server)
|
||||
} catch (error) {
|
||||
isLoading.value = false
|
||||
logger.error('Failed to delete trusted server', { error })
|
||||
showError(t('federation', 'Failed to delete trusted server. Please try again later.'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<li :class="$style.trustedServer">
|
||||
<NcIconSvgWrapper
|
||||
:class="{
|
||||
[$style.trustedServer__icon_error]: hasError,
|
||||
}"
|
||||
:path="serverIcon"
|
||||
:name="serverStatus[0]"
|
||||
:title="serverStatus[1]" />
|
||||
|
||||
<code :class="$style.trustedServer__url" v-text="server.url" />
|
||||
|
||||
<NcButton
|
||||
:aria-label="t('federation', 'Delete')"
|
||||
:title="t('federation', 'Delete')"
|
||||
:disabled="isLoading"
|
||||
@click="onDelete">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="isLoading" />
|
||||
<NcIconSvgWrapper v-else :path="mdiTrashCanOutline" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.trustedServer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: var(--default-grid-baseline);
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius-element);
|
||||
padding-inline-start: var(--default-grid-baseline);
|
||||
}
|
||||
|
||||
.trustedServer:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
|
||||
.trustedServer__icon_error {
|
||||
color: var(--color-element-error);
|
||||
}
|
||||
|
||||
.trustedServer__url {
|
||||
padding-inline: 1ch;
|
||||
flex: 1 0 auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { http, HttpResponse } from 'msw'
|
||||
import { setupServer } from 'msw/node'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'
|
||||
import { addServer, ApiError, deleteServer, TrustedServerStatus } from './api.ts'
|
||||
|
||||
export const handlers = [
|
||||
http.post('/ocs/v2.php/apps/federation/trusted-servers', async ({ request }) => {
|
||||
const { url } = (await request.json()) as { url: string }
|
||||
if (url === 'https://network-error.com') {
|
||||
return HttpResponse.error()
|
||||
}
|
||||
if (url === 'https://existing-server.com') {
|
||||
return HttpResponse.json({
|
||||
ocs: {
|
||||
meta: {
|
||||
status: 'failure',
|
||||
statuscode: 409,
|
||||
message: 'Server already exists',
|
||||
},
|
||||
},
|
||||
}, { status: 409 })
|
||||
}
|
||||
|
||||
return HttpResponse.json({
|
||||
ocs: {
|
||||
meta: {
|
||||
status: 'ok',
|
||||
},
|
||||
data: {
|
||||
id: 1,
|
||||
url,
|
||||
},
|
||||
},
|
||||
})
|
||||
}),
|
||||
http.delete('/ocs/v2.php/apps/federation/trusted-servers/:id', async ({ params }) => {
|
||||
if (params.id === '1') {
|
||||
return HttpResponse.json({
|
||||
ocs: {
|
||||
meta: {
|
||||
status: 'ok',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
if (params.id === '2') {
|
||||
return HttpResponse.json({
|
||||
ocs: {
|
||||
meta: {
|
||||
status: 'failure',
|
||||
statuscode: 404,
|
||||
message: 'Server does not exist',
|
||||
},
|
||||
},
|
||||
}, { status: 404 })
|
||||
}
|
||||
|
||||
return HttpResponse.error()
|
||||
}),
|
||||
]
|
||||
|
||||
const server = setupServer(...handlers)
|
||||
beforeAll(() => server.listen())
|
||||
afterEach(() => server.resetHandlers())
|
||||
afterAll(() => server.close())
|
||||
|
||||
describe('addServer', () => {
|
||||
test('returns a trusted server object on success', async () => {
|
||||
const server = await addServer('https://trusted-server.com')
|
||||
expect(server).toEqual({
|
||||
id: 1,
|
||||
url: 'https://trusted-server.com',
|
||||
status: TrustedServerStatus.STATUS_PENDING,
|
||||
})
|
||||
})
|
||||
|
||||
test('throws API error when already added', async () => {
|
||||
await expect(() => addServer('https://existing-server.com')).rejects.toThrowError(ApiError)
|
||||
await expect(() => addServer('https://existing-server.com')).rejects.toThrow('Server already exists')
|
||||
})
|
||||
|
||||
test('throws error when network error occurs', async () => {
|
||||
await expect(() => addServer('https://network-error.com')).rejects.toThrowError(Error)
|
||||
await expect(() => addServer('https://network-error.com')).rejects.not.toThrowError(ApiError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('deleteServer', () => {
|
||||
test('resolves on success', async () => {
|
||||
await expect(deleteServer(1)).resolves.not.toThrow()
|
||||
})
|
||||
|
||||
test('throws API error when already added', async () => {
|
||||
await expect(() => deleteServer(2)).rejects.toThrowError(ApiError)
|
||||
await expect(() => deleteServer(2)).rejects.toThrow('Server does not exist')
|
||||
})
|
||||
|
||||
test('throws error when network error occurs', async () => {
|
||||
await expect(() => deleteServer(3)).rejects.toThrowError(Error)
|
||||
await expect(() => deleteServer(3)).rejects.not.toThrowError(ApiError)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { OCSResponse } from '@nextcloud/typings/ocs'
|
||||
|
||||
import axios, { isAxiosError } from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
|
||||
export const TrustedServerStatus = Object.freeze({
|
||||
/** after a user list was exchanged at least once successfully */
|
||||
STATUS_OK: 1,
|
||||
/** waiting for shared secret or initial user list exchange */
|
||||
STATUS_PENDING: 2,
|
||||
/** something went wrong, misconfigured server, software bug,... user interaction needed */
|
||||
STATUS_FAILURE: 3,
|
||||
/** remote server revoked access */
|
||||
STATUS_ACCESS_REVOKED: 4,
|
||||
})
|
||||
|
||||
export interface ITrustedServer {
|
||||
id: number
|
||||
url: string
|
||||
status: typeof TrustedServerStatus[keyof typeof TrustedServerStatus]
|
||||
}
|
||||
|
||||
export class ApiError extends Error {}
|
||||
|
||||
/**
|
||||
* Add a new trusted server
|
||||
*
|
||||
* @param url - The new URL to add
|
||||
*/
|
||||
export async function addServer(url: string): Promise<ITrustedServer> {
|
||||
try {
|
||||
const { data } = await axios.post<OCSResponse<Omit<ITrustedServer, 'status'>>>(
|
||||
generateOcsUrl('apps/federation/trusted-servers'),
|
||||
{ url },
|
||||
)
|
||||
|
||||
const serverData = data.ocs.data
|
||||
return {
|
||||
id: serverData.id,
|
||||
url: serverData.url,
|
||||
status: TrustedServerStatus.STATUS_PENDING,
|
||||
}
|
||||
} catch (error) {
|
||||
throw mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id - The id of the trusted server to remove
|
||||
*/
|
||||
export async function deleteServer(id: number): Promise<void> {
|
||||
try {
|
||||
await axios.delete(generateOcsUrl(`apps/federation/trusted-servers/${id}`))
|
||||
} catch (error) {
|
||||
throw mapError(error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Error handling for API calls
|
||||
*
|
||||
* @param error - The catch error
|
||||
*/
|
||||
function mapError(error: unknown): ApiError | unknown {
|
||||
if (isAxiosError(error) && error.response?.data?.ocs) {
|
||||
return new ApiError((error.response.data as OCSResponse).ocs.meta.message, { cause: error })
|
||||
}
|
||||
return error
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
export const logger = getLoggerBuilder().setApp('federation').build()
|
||||
@@ -0,0 +1,10 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import AdminSettings from './views/AdminSettings.vue'
|
||||
|
||||
const app = createApp(AdminSettings)
|
||||
app.mount('#federation-admin-settings')
|
||||
@@ -0,0 +1,91 @@
|
||||
<!--
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ITrustedServer } from '../services/api.ts'
|
||||
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref } from 'vue'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import AddTrustedServerForm from '../components/AddTrustedServerForm.vue'
|
||||
import TrustedServer from '../components/TrustedServer.vue'
|
||||
import { TrustedServerStatus } from '../services/api.ts'
|
||||
|
||||
const adminSettings = loadState<{ docUrl: string, trustedServers: ITrustedServer[] }>('federation', 'adminSettings')
|
||||
const trustedServers = ref(adminSettings.trustedServers)
|
||||
const showPendingServerInfo = computed(() => trustedServers.value.some((server) => server.status === TrustedServerStatus.STATUS_PENDING))
|
||||
|
||||
/**
|
||||
* Handle add trusted server form submission
|
||||
*
|
||||
* @param server - The server to add
|
||||
*/
|
||||
async function onAdd(server: ITrustedServer) {
|
||||
trustedServers.value.unshift(server)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle delete trusted server event
|
||||
*
|
||||
* @param server - The server to delete
|
||||
*/
|
||||
function onDelete(server: ITrustedServer) {
|
||||
trustedServers.value = trustedServers.value.filter((s) => s.id !== server.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcSettingsSection
|
||||
:name="t('federation', 'Trusted servers')"
|
||||
:doc-url="adminSettings.docUrl"
|
||||
:description="t('federation', 'Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share.')">
|
||||
<NcNoteCard
|
||||
v-if="showPendingServerInfo"
|
||||
type="info"
|
||||
:text="t('federation', 'Each server must validate the other. This process may require a few cron cycles.')" />
|
||||
|
||||
<TransitionGroup
|
||||
:class="$style.federationAdminSettings__trustedServersList"
|
||||
:aria-label="t('federation', 'Trusted servers')"
|
||||
tag="ul"
|
||||
:enter-from-class="$style.transition_hidden"
|
||||
:enter-active-class="$style.transition_active"
|
||||
:leave-active-class="$style.transition_active"
|
||||
:leave-to-class="$style.transition_hidden">
|
||||
<TrustedServer
|
||||
v-for="server in trustedServers"
|
||||
:key="server.id"
|
||||
:class="$style.federationAdminSettings__trustedServersListItem"
|
||||
:server="server"
|
||||
@delete="onDelete" />
|
||||
</TransitionGroup>
|
||||
|
||||
<AddTrustedServerForm @add="onAdd" />
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.federationAdminSettings__trustedServersList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
.federationAdminSettings__trustedServersListItem {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.transition_active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
|
||||
.transition_hidden {
|
||||
opacity: 0;
|
||||
transform: translateX(30px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,64 +1,7 @@
|
||||
<?php
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
use OCA\Federation\TrustedServers;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Server;
|
||||
use OCP\Util;
|
||||
|
||||
/** @var IL10N $l */
|
||||
|
||||
Util::addScript('federation', 'settings-admin');
|
||||
Util::addStyle('federation', 'settings-admin');
|
||||
|
||||
$urlGenerator = Server::get(IURLGenerator::class);
|
||||
$documentationLink = $urlGenerator->linkToDocs('admin-sharing-federated') . '#configuring-trusted-nextcloud-servers';
|
||||
$documentationLabel = $l->t('External documentation for Federated Cloud Sharing');
|
||||
?>
|
||||
<div id="ocFederationSettings" class="section">
|
||||
<h2>
|
||||
<?php p($l->t('Trusted servers')); ?>
|
||||
<a target="_blank" rel="noreferrer noopener" class="icon-info"
|
||||
title="<?php p($documentationLabel);?>"
|
||||
href="<?php p($documentationLink); ?>"></a>
|
||||
</h2>
|
||||
<p class="settings-hint"><?php p($l->t('Federation allows you to connect with other trusted servers to exchange the account directory. For example this will be used to auto-complete external accounts for federated sharing. It is not necessary to add a server as trusted server in order to create a federated share.')); ?></p>
|
||||
<p class="settings-hint"><?php p($l->t('Each server must validate the other. This process may require a few cron cycles.')); ?></p>
|
||||
|
||||
<ul id="listOfTrustedServers">
|
||||
<?php foreach ($_['trustedServers'] as $trustedServer) { ?>
|
||||
<li id="<?php p($trustedServer['id']); ?>">
|
||||
<?php if ((int)$trustedServer['status'] === TrustedServers::STATUS_OK) { ?>
|
||||
<span class="status success"></span>
|
||||
<?php
|
||||
} elseif (
|
||||
(int)$trustedServer['status'] === TrustedServers::STATUS_PENDING
|
||||
|| (int)$trustedServer['status'] === TrustedServers::STATUS_ACCESS_REVOKED
|
||||
) { ?>
|
||||
<span class="status indeterminate"></span>
|
||||
<?php } else {?>
|
||||
<span class="status error"></span>
|
||||
<?php } ?>
|
||||
<?php p($trustedServer['url']); ?>
|
||||
<span class="icon icon-delete"></span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
|
||||
<div id="ocFederationAddServer">
|
||||
<button id="ocFederationAddServerButton"><?php p($l->t('+ Add trusted server')); ?></button>
|
||||
<div class="serverUrl hidden">
|
||||
<div class="serverUrl-block">
|
||||
<label for="serverUrl"><?php p($l->t('Trusted server')); ?></label>
|
||||
<input id="serverUrl" type="text" value="" placeholder="<?php p($l->t('Trusted server')); ?>" name="server_url"/>
|
||||
<button id="ocFederationSubmit" class="hidden"><?php p($l->t('Add')); ?></button>
|
||||
</div>
|
||||
<span class="msg"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="federation-admin-settings"></div>
|
||||
@@ -10,24 +10,35 @@ namespace OCA\Federation\Tests\Settings;
|
||||
use OCA\Federation\Settings\Admin;
|
||||
use OCA\Federation\TrustedServers;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class AdminTest extends TestCase {
|
||||
private TrustedServers&MockObject $trustedServers;
|
||||
private IInitialState&MockObject $initialState;
|
||||
private IURLGenerator&MockObject $urlGenerator;
|
||||
private Admin $admin;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->trustedServers = $this->createMock(TrustedServers::class);
|
||||
$this->initialState = $this->createMock(IInitialState::class);
|
||||
$this->urlGenerator = $this->createMock(IURLGenerator::class);
|
||||
$this->admin = new Admin(
|
||||
$this->trustedServers,
|
||||
$this->initialState,
|
||||
$this->urlGenerator,
|
||||
$this->createMock(IL10N::class)
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetForm(): void {
|
||||
$this->urlGenerator->method('linkToDocs')
|
||||
->with('admin-sharing-federated')
|
||||
->willReturn('docs://federated_sharing');
|
||||
$this->trustedServers
|
||||
->expects($this->once())
|
||||
->method('getServers')
|
||||
@@ -35,8 +46,15 @@ class AdminTest extends TestCase {
|
||||
|
||||
$params = [
|
||||
'trustedServers' => ['myserver', 'secondserver'],
|
||||
'docUrl' => 'docs://federated_sharing#configuring-trusted-nextcloud-servers',
|
||||
];
|
||||
$expected = new TemplateResponse('federation', 'settings-admin', $params, '');
|
||||
|
||||
$this->initialState
|
||||
->expects($this->once())
|
||||
->method('provideInitialState')
|
||||
->with('adminSettings', $params);
|
||||
|
||||
$expected = new TemplateResponse('federation', 'settings-admin', renderAs: '');
|
||||
$this->assertEquals($expected, $this->admin->getForm());
|
||||
}
|
||||
|
||||
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../../apps/federation
|
||||
@@ -12,6 +12,9 @@ const modules = {
|
||||
'settings-admin-example-content': resolve(import.meta.dirname, 'apps/dav/src', 'settings-admin-example-content.ts'),
|
||||
'settings-personal-availability': resolve(import.meta.dirname, 'apps/dav/src', 'settings-personal-availability.ts'),
|
||||
},
|
||||
federation: {
|
||||
'settings-admin': resolve(import.meta.dirname, 'apps/federation/src', 'settings-admin.ts'),
|
||||
},
|
||||
federatedfilesharing: {
|
||||
'init-files': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'init-files.js'),
|
||||
'settings-admin': resolve(import.meta.dirname, 'apps/federatedfilesharing/src', 'settings-admin.ts'),
|
||||
|
||||
Generated
+394
@@ -65,6 +65,7 @@
|
||||
"is-svg": "^6.1.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"jsdom-testing-mocks": "^1.16.0",
|
||||
"msw": "^2.12.7",
|
||||
"sass": "^1.97.1",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-use-logical": "^2.1.2",
|
||||
@@ -1731,6 +1732,122 @@
|
||||
"url": "https://github.com/sponsors/nzakas"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/ansi": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/confirm": {
|
||||
"version": "5.1.21",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz",
|
||||
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/core": "^10.3.2",
|
||||
"@inquirer/type": "^3.0.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core": {
|
||||
"version": "10.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
|
||||
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@inquirer/ansi": "^1.0.2",
|
||||
"@inquirer/figures": "^1.0.15",
|
||||
"@inquirer/type": "^3.0.10",
|
||||
"cli-width": "^4.1.0",
|
||||
"mute-stream": "^2.0.0",
|
||||
"signal-exit": "^4.1.0",
|
||||
"wrap-ansi": "^6.2.0",
|
||||
"yoctocolors-cjs": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/core/node_modules/wrap-ansi": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/figures": {
|
||||
"version": "1.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@inquirer/type": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.10.tgz",
|
||||
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">=18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/balanced-match": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||
@@ -2093,6 +2210,24 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@mswjs/interceptors": {
|
||||
"version": "0.40.0",
|
||||
"resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.40.0.tgz",
|
||||
"integrity": "sha512-EFd6cVbHsgLa6wa4RljGj6Wk75qoHxUSyc5asLyyPSyuhIcdS2Q3Phw6ImS1q+CkALthJRShiYfKANcQMuMqsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@open-draft/deferred-promise": "^2.2.0",
|
||||
"@open-draft/logger": "^0.3.0",
|
||||
"@open-draft/until": "^2.0.0",
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.3",
|
||||
"strict-event-emitter": "^0.5.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@nextcloud/auth": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/auth/-/auth-2.5.3.tgz",
|
||||
@@ -2627,6 +2762,31 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@open-draft/deferred-promise": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
|
||||
"integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@open-draft/logger": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
|
||||
"integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@open-draft/until": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
|
||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@parcel/watcher": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.1.tgz",
|
||||
@@ -3938,6 +4098,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/statuses": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz",
|
||||
"integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/tmp": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/tmp/-/tmp-0.2.6.tgz",
|
||||
@@ -6053,6 +6220,16 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-width": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
@@ -6278,6 +6455,20 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
|
||||
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/copy-anything": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz",
|
||||
@@ -9095,6 +9286,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/graphql": {
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.12.0.tgz",
|
||||
"integrity": "sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/has-bigints": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz",
|
||||
@@ -9304,6 +9505,13 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/headers-polyfill": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
|
||||
"integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "11.11.1",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
|
||||
@@ -9940,6 +10148,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/is-node-process": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
|
||||
"integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -12012,6 +12227,101 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw": {
|
||||
"version": "2.12.7",
|
||||
"resolved": "https://registry.npmjs.org/msw/-/msw-2.12.7.tgz",
|
||||
"integrity": "sha512-retd5i3xCZDVWMYjHEVuKTmhqY8lSsxujjVrZiGbbdoxxIBg5S7rCuYy/YQpfrTYIxpd/o0Kyb/3H+1udBMoYg==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@inquirer/confirm": "^5.0.0",
|
||||
"@mswjs/interceptors": "^0.40.0",
|
||||
"@open-draft/deferred-promise": "^2.2.0",
|
||||
"@types/statuses": "^2.0.6",
|
||||
"cookie": "^1.0.2",
|
||||
"graphql": "^16.12.0",
|
||||
"headers-polyfill": "^4.0.2",
|
||||
"is-node-process": "^1.2.0",
|
||||
"outvariant": "^1.4.3",
|
||||
"path-to-regexp": "^6.3.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"rettime": "^0.7.0",
|
||||
"statuses": "^2.0.2",
|
||||
"strict-event-emitter": "^0.5.1",
|
||||
"tough-cookie": "^6.0.0",
|
||||
"type-fest": "^5.2.0",
|
||||
"until-async": "^3.0.2",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"msw": "cli/index.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/mswjs"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">= 4.8.x"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/tldts": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz",
|
||||
"integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tldts-core": "^7.0.19"
|
||||
},
|
||||
"bin": {
|
||||
"tldts": "bin/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/tldts-core": {
|
||||
"version": "7.0.19",
|
||||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz",
|
||||
"integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msw/node_modules/tough-cookie": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz",
|
||||
"integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tldts": "^7.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/msw/node_modules/type-fest": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.3.1.tgz",
|
||||
"integrity": "sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==",
|
||||
"dev": true,
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"dependencies": {
|
||||
"tagged-tag": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/muggle-string": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.4.1.tgz",
|
||||
@@ -12019,6 +12329,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mute-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "^18.17.0 || >=20.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/nan": {
|
||||
"version": "2.23.0",
|
||||
"resolved": "https://registry.npmjs.org/nan/-/nan-2.23.0.tgz",
|
||||
@@ -12384,6 +12704,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/outvariant": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
|
||||
"integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/p-limit": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
||||
@@ -12665,6 +12992,13 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
|
||||
"integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
|
||||
@@ -13691,6 +14025,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/rettime": {
|
||||
"version": "0.7.0",
|
||||
"resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz",
|
||||
"integrity": "sha512-LPRKoHnLKd/r3dVxcwO7vhCW+orkOGj9ViueosEBK6ie89CijnfRlhaDhHq/3Hxu4CkWQtxwlBG0mzTQY6uQjw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
|
||||
@@ -14605,6 +14946,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/std-env": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
|
||||
@@ -14680,6 +15031,13 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-event-emitter": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
|
||||
"integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
@@ -15426,6 +15784,19 @@
|
||||
"url": "https://github.com/chalk/slice-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tagged-tag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz",
|
||||
"integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
|
||||
@@ -16043,6 +16414,16 @@
|
||||
"node": ">= 10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/until-async": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz",
|
||||
"integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kettanaito"
|
||||
}
|
||||
},
|
||||
"node_modules/untildify": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz",
|
||||
@@ -17497,6 +17878,19 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/yoctocolors-cjs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zwitch": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"is-svg": "^6.1.0",
|
||||
"jsdom": "^27.4.0",
|
||||
"jsdom-testing-mocks": "^1.16.0",
|
||||
"msw": "^2.12.7",
|
||||
"sass": "^1.97.1",
|
||||
"stylelint": "^16.26.1",
|
||||
"stylelint-use-logical": "^2.1.2",
|
||||
|
||||
Reference in New Issue
Block a user