diff options
author | Maksim Sukharev <antreesy.web@gmail.com> | 2024-06-24 09:36:44 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-24 09:36:44 +0200 |
commit | f4d27ca853e242db1ecb3b4392d7a966be1ac759 (patch) | |
tree | 92990d1a9b85306b3f618853eae66fa2e3a47a6f /src | |
parent | 5f2f5dc352479853c74bafb7edcc7a45c7babfed (diff) | |
parent | 6d212600bf641587d40edab93536111bd1e4845a (diff) |
Merge pull request #12434 from nextcloud/feat/12259/ban-endpoints
feat(ban): add endpoints for ban handling
Diffstat (limited to 'src')
-rw-r--r-- | src/App.vue | 4 | ||||
-rw-r--r-- | src/__mocks__/router.js | 6 | ||||
-rw-r--r-- | src/components/ConversationSettings/BanSettings/BanSettings.vue | 132 | ||||
-rw-r--r-- | src/components/ConversationSettings/BanSettings/BannedItem.vue | 101 | ||||
-rw-r--r-- | src/components/ConversationSettings/ConversationSettingsDialog.vue | 7 | ||||
-rw-r--r-- | src/components/RightSidebar/Participants/Participant.spec.js | 82 | ||||
-rw-r--r-- | src/components/RightSidebar/Participants/Participant.vue | 31 | ||||
-rw-r--r-- | src/router/router.js | 7 | ||||
-rw-r--r-- | src/services/banService.ts | 56 | ||||
-rw-r--r-- | src/store/participantsStore.js | 22 | ||||
-rw-r--r-- | src/types/index.ts | 9 | ||||
-rw-r--r-- | src/views/ForbiddenView.vue | 34 |
12 files changed, 488 insertions, 3 deletions
diff --git a/src/App.vue b/src/App.vue index b8457d26d..d8d3dc669 100644 --- a/src/App.vue +++ b/src/App.vue @@ -378,6 +378,10 @@ export default { } }) + EventBus.on('forbidden-route', (params) => { + this.$router.push({ name: 'forbidden' }) + }) + /** * Listens to the conversationsReceived globalevent, emitted by the conversationsList * component each time a new batch of conversations is received and processed in diff --git a/src/__mocks__/router.js b/src/__mocks__/router.js index 613e74aa4..f62163e43 100644 --- a/src/__mocks__/router.js +++ b/src/__mocks__/router.js @@ -25,6 +25,12 @@ export default new VueRouter({ props: true, }, { + path: '/apps/spreed/forbidden', + name: 'forbidden', + component: Stub, + props: true, + }, + { path: '/apps/spreed/duplicate-session', name: 'duplicatesession', component: Stub, diff --git a/src/components/ConversationSettings/BanSettings/BanSettings.vue b/src/components/ConversationSettings/BanSettings/BanSettings.vue new file mode 100644 index 000000000..cc40d4487 --- /dev/null +++ b/src/components/ConversationSettings/BanSettings/BanSettings.vue @@ -0,0 +1,132 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <div class="conversation-ban__settings"> + <h4 class="app-settings-section__subtitle"> + {{ t('spreed', 'Banned users') }} + </h4> + <div class="app-settings-section__hint"> + {{ t('spreed', 'Manage the list of banned users in this conversation.') }} + </div> + <NcButton @click="modal = true"> + {{ t('spreed', 'Manage bans') }} + </NcButton> + + <NcModal v-if="modal" + container=".conversation-ban__settings" + @close="modal = false"> + <div class="conversation-ban__content"> + <h2 class="conversation-ban__title"> + {{ t('spreed', 'Banned users') }} + </h2> + + <ul v-if="banList.length" class="conversation-ban__list"> + <BannedItem v-for="ban in banList" + :key="ban.id" + :ban="ban" + @unban-participant="handleUnban(ban.id)" /> + </ul> + + <NcEmptyContent v-else> + <template #icon> + <NcLoadingIcon v-if="isLoading" /> + <AccountCancel v-else /> + </template> + + <template #description> + <p>{{ isLoading ? t('spreed', 'Loading …') : t('spreed', 'No banned users') }}</p> + </template> + </NcEmptyContent> + </div> + </NcModal> + </div> +</template> + +<script> +import AccountCancel from 'vue-material-design-icons/AccountCancel.vue' + +import { t } from '@nextcloud/l10n' + +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent.js' +import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon.js' +import NcModal from '@nextcloud/vue/dist/Components/NcModal.js' + +import BannedItem from './BannedItem.vue' + +import { getConversationBans, unbanActor } from '../../../services/banService.ts' + +export default { + name: 'BanSettings', + + components: { + NcButton, + NcEmptyContent, + NcLoadingIcon, + NcModal, + BannedItem, + // Icons + AccountCancel, + }, + + props: { + token: { + type: String, + required: true, + }, + }, + + data() { + return { + banList: [], + isLoading: true, + modal: false, + } + }, + + watch: { + modal(value) { + if (value) { + this.getList() + } + } + }, + + methods: { + t, + + async getList() { + this.isLoading = true + const response = await getConversationBans(this.token) + this.banList = response.data.ocs.data + this.isLoading = false + }, + + async handleUnban(id) { + await unbanActor(this.token, id) + this.banList = this.banList.filter(ban => ban.id !== id) + } + }, +} +</script> + +<style lang="scss" scoped> +.conversation-ban { + &__content { + min-height: 250px; + } + + &__title { + text-align: center; + } + + &__list { + overflow: auto; + height: calc(100% - 45px - 12px); + padding: calc(var(--default-grid-baseline) * 2); + } +} +</style> diff --git a/src/components/ConversationSettings/BanSettings/BannedItem.vue b/src/components/ConversationSettings/BanSettings/BannedItem.vue new file mode 100644 index 000000000..cddd48399 --- /dev/null +++ b/src/components/ConversationSettings/BanSettings/BannedItem.vue @@ -0,0 +1,101 @@ +<!-- + - SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + - SPDX-License-Identifier: AGPL-3.0-or-later +--> + +<template> + <li :key="ban.id" class="ban-item"> + <div class="ban-item__header"> + <span class="ban-item__caption">{{ ban.bannedId }}</span> + <div class="ban-item__buttons"> + <NcButton type="tertiary" @click="showDetails = !showDetails"> + {{ showDetails ? t('spreed', 'Hide details') : t('spreed', 'Show details') }} + </NcButton> + <NcButton @click="$emit('unban-participant')"> + {{ t('spreed', 'Unban') }} + </NcButton> + </div> + </div> + <ul v-if="showDetails" class="ban-item__hint"> + <!-- eslint-disable-next-line vue/no-v-html --> + <li v-for="(item, index) in banInfo" :key="index" v-html="item" /> + </ul> + </li> +</template> + +<script> +import { t } from '@nextcloud/l10n' +import moment from '@nextcloud/moment' + +import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' + +export default { + name: 'BannedItem', + + components: { + NcButton, + }, + + props: { + ban: { + type: Object, + required: true, + }, + }, + + emits: ['unban-participant'], + + data() { + return { + showDetails: false, + } + }, + + computed: { + banInfo() { + return [ + t('spreed', '<strong>Banned by:</strong> {actor}', { actor: this.ban.actorId }, + undefined, { escape: false, sanitize: false }), + t('spreed', '<strong>Date:</strong> {date}', { date: moment(this.ban.bannedTime * 1000).format('lll') }, + undefined, { escape: false, sanitize: false }), + t('spreed', '<strong>Note:</strong> {note}', { note: this.ban.internalNote }, + undefined, { escape: false, sanitize: false }), + ] + }, + }, + + methods: { + t, + }, +} +</script> + +<style lang="scss" scoped> + +.ban-item { + padding: 4px 0; + &:not(:last-child) { + border-bottom: 1px solid var(--color-border); + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + } + + &__caption { + font-weight: bold; + } + + &__hint { + word-wrap: break-word; + color: var(--color-text-maxcontrast); + margin-bottom: 4px; + } + + &__buttons { + display: flex; + } +} +</style> diff --git a/src/components/ConversationSettings/ConversationSettingsDialog.vue b/src/components/ConversationSettings/ConversationSettingsDialog.vue index 3720cc789..782e9b869 100644 --- a/src/components/ConversationSettings/ConversationSettingsDialog.vue +++ b/src/components/ConversationSettings/ConversationSettingsDialog.vue @@ -40,6 +40,7 @@ <LinkShareSettings v-if="!isNoteToSelf" :token="token" :can-moderate="canFullModerate" /> <RecordingConsentSettings v-if="!isNoteToSelf && recordingConsentAvailable" :token="token" :can-moderate="selfIsOwnerOrModerator" /> <ExpirationSettings :token="token" :can-moderate="selfIsOwnerOrModerator" /> + <BanSettings v-if="supportBanV1 && canFullModerate" :token="token" /> </NcAppSettingsSection> <!-- Meeting: lobby and sip --> @@ -100,6 +101,7 @@ import NcAppSettingsDialog from '@nextcloud/vue/dist/Components/NcAppSettingsDia import NcAppSettingsSection from '@nextcloud/vue/dist/Components/NcAppSettingsSection.js' import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' +import BanSettings from './BanSettings/BanSettings.vue' import BasicInfo from './BasicInfo.vue' import BotsSettings from './BotsSettings.vue' import BreakoutRoomsSettings from './BreakoutRoomsSettings.vue' @@ -123,6 +125,7 @@ export default { name: 'ConversationSettingsDialog', components: { + BanSettings, BasicInfo, BotsSettings, BreakoutRoomsSettings, @@ -180,6 +183,10 @@ export default { return (!hasTalkFeature(this.token, 'federation-v1') || !this.conversation.remoteServer) }, + supportBanV1() { + return hasTalkFeature(this.token, 'ban-v1') + }, + showMediaSettings() { return this.settingsStore.getShowMediaSettings(this.token) }, diff --git a/src/components/RightSidebar/Participants/Participant.spec.js b/src/components/RightSidebar/Participants/Participant.spec.js index 6dc3eaf88..b69bad0e1 100644 --- a/src/components/RightSidebar/Participants/Participant.spec.js +++ b/src/components/RightSidebar/Participants/Participant.spec.js @@ -14,7 +14,10 @@ import VideoIcon from 'vue-material-design-icons/Video.vue' import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton.js' import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcInputField from '@nextcloud/vue/dist/Components/NcInputField.js' +import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import Participant from './Participant.vue' import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue' @@ -107,7 +110,10 @@ describe('Participant.vue', () => { stubs: { NcActionButton, NcButton, + NcCheckboxRadioSwitch, NcDialog, + NcInputField, + NcTextField, }, directives: { tooltip: tooltipMock, @@ -639,6 +645,8 @@ describe('Participant.vue', () => { expect(removeAction).toHaveBeenCalledWith(expect.anything(), { token: 'current-token', attendeeId: 'alice-attendee-id', + banParticipant: false, + internalNote: '', }) } @@ -651,6 +659,56 @@ describe('Participant.vue', () => { expect(actionButton.exists()).toBe(false) } + /** + * @param {string} buttonText Label of the remove action to find + * @param {string} internalNote text of provided note + */ + async function testCanBan(buttonText = 'Remove participant', internalNote = 'test note') { + const wrapper = mountParticipant(participant) + const actionButton = findNcActionButton(wrapper, buttonText) + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + const dialog = wrapper.findComponent(NcDialog) + expect(dialog.exists()).toBeTruthy() + + const checkbox = dialog.findComponent(NcCheckboxRadioSwitch) + await checkbox.find('input').trigger('change') + + const input = dialog.findComponent(NcTextField) + expect(input.exists()).toBeTruthy() + input.find('input').setValue(internalNote) + await input.find('input').trigger('change') + + const button = findNcButton(dialog, 'Remove') + await button.find('button').trigger('click') + + expect(removeAction).toHaveBeenCalledWith(expect.anything(), { + token: 'current-token', + attendeeId: 'alice-attendee-id', + banParticipant: true, + internalNote + }) + } + + /** + * @param {string} buttonText Label of the remove action to find + */ + async function testCannotBan(buttonText = 'Remove participant') { + const wrapper = mountParticipant(participant) + const actionButton = findNcActionButton(wrapper, buttonText) + expect(actionButton.exists()).toBe(true) + + await actionButton.find('button').trigger('click') + + const dialog = wrapper.findComponent(NcDialog) + expect(dialog.exists()).toBeTruthy() + + const checkbox = dialog.findComponent(NcCheckboxRadioSwitch) + expect(checkbox.exists()).toBeFalsy() + } + test('allows a moderator to remove a moderator', async () => { conversation.participantType = PARTICIPANT.TYPE.MODERATOR participant.participantType = PARTICIPANT.TYPE.MODERATOR @@ -707,6 +765,30 @@ describe('Participant.vue', () => { conversation.participantType = PARTICIPANT.TYPE.USER await testCannotRemove() }) + + test('allows a moderator to ban a moderator', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.USER + await testCanBan() + }) + + test('allows a moderator to ban a guest', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.GUEST + await testCanBan() + }) + + test('does not allow a moderator to ban a moderator', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.participantType = PARTICIPANT.TYPE.MODERATOR + await testCannotBan() + }) + + test('does not allow a moderator to ban a group', async () => { + conversation.participantType = PARTICIPANT.TYPE.MODERATOR + participant.actorType = ATTENDEE.ACTOR_TYPE.GROUPS + await testCannotBan('Remove group and members') + }) }) describe('dial-in PIN', () => { /** diff --git a/src/components/RightSidebar/Participants/Participant.vue b/src/components/RightSidebar/Participants/Participant.vue index 3d344ee2e..921258c3c 100644 --- a/src/components/RightSidebar/Participants/Participant.vue +++ b/src/components/RightSidebar/Participants/Participant.vue @@ -313,6 +313,17 @@ :name="removeParticipantLabel" :container="container"> <p> {{ removeDialogMessage }} </p> + <template v-if="supportBanV1 && showPermissionsOptions"> + <NcCheckboxRadioSwitch :checked.sync="isBanParticipant"> + {{ t('spreed', 'Also ban from this conversation') }} + </NcCheckboxRadioSwitch> + <template v-if="isBanParticipant"> + <NcTextField v-if="isBanParticipant" + class="participant-dialog__input" + :label="t('spreed', 'Internal note (reason to ban)')" + :value.sync="internalNote" /> + </template> + </template> <template #actions> <NcButton type="tertiary" @click="isRemoveDialogOpen = false"> {{ t('spreed', 'Dismiss') }} @@ -363,7 +374,9 @@ import NcActions from '@nextcloud/vue/dist/Components/NcActions.js' import NcActionSeparator from '@nextcloud/vue/dist/Components/NcActionSeparator.js' import NcActionText from '@nextcloud/vue/dist/Components/NcActionText.js' import NcButton from '@nextcloud/vue/dist/Components/NcButton.js' +import NcCheckboxRadioSwitch from '@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch.js' import NcDialog from '@nextcloud/vue/dist/Components/NcDialog.js' +import NcTextField from '@nextcloud/vue/dist/Components/NcTextField.js' import Tooltip from '@nextcloud/vue/dist/Directives/Tooltip.js' import ParticipantPermissionsEditor from './ParticipantPermissionsEditor.vue' @@ -396,7 +409,9 @@ export default { NcActionText, NcActionSeparator, NcButton, + NcCheckboxRadioSwitch, NcDialog, + NcTextField, ParticipantPermissionsEditor, // Icons Account, @@ -470,6 +485,8 @@ export default { isStatusTooltipVisible: false, permissionsEditor: false, isRemoveDialogOpen: false, + isBanParticipant: false, + internalNote: '', disabled: false, } }, @@ -835,6 +852,10 @@ export default { || this.participant.actorType === ATTENDEE.ACTOR_TYPE.EMAILS) }, + supportBanV1() { + return hasTalkFeature(this.token, 'ban-v1') + }, + isLobbyEnabled() { return this.conversation.lobbyState === WEBINAR.LOBBY.NON_MODERATORS }, @@ -977,7 +998,11 @@ export default { await this.$store.dispatch('removeParticipant', { token: this.token, attendeeId: this.attendeeId, + banParticipant: this.isBanParticipant, + internalNote: this.internalNote, }) + this.isBanParticipant = false + this.internalNote = '' this.isRemoveDialogOpen = false }, @@ -1224,6 +1249,12 @@ export default { cursor: pointer; } +.participant-dialog { + &__input { + margin-block-end: 6px; + } +} + .utils { &__checkmark { margin-right: 11px; diff --git a/src/router/router.js b/src/router/router.js index 42c458abf..6104a32e6 100644 --- a/src/router/router.js +++ b/src/router/router.js @@ -9,6 +9,7 @@ import Router from 'v |