summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorMaksim Sukharev <antreesy.web@gmail.com>2024-06-24 09:36:44 +0200
committerGitHub <noreply@github.com>2024-06-24 09:36:44 +0200
commitf4d27ca853e242db1ecb3b4392d7a966be1ac759 (patch)
tree92990d1a9b85306b3f618853eae66fa2e3a47a6f /src
parent5f2f5dc352479853c74bafb7edcc7a45c7babfed (diff)
parent6d212600bf641587d40edab93536111bd1e4845a (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.vue4
-rw-r--r--src/__mocks__/router.js6
-rw-r--r--src/components/ConversationSettings/BanSettings/BanSettings.vue132
-rw-r--r--src/components/ConversationSettings/BanSettings/BannedItem.vue101
-rw-r--r--src/components/ConversationSettings/ConversationSettingsDialog.vue7
-rw-r--r--src/components/RightSidebar/Participants/Participant.spec.js82
-rw-r--r--src/components/RightSidebar/Participants/Participant.vue31
-rw-r--r--src/router/router.js7
-rw-r--r--src/services/banService.ts56
-rw-r--r--src/store/participantsStore.js22
-rw-r--r--src/types/index.ts9
-rw-r--r--src/views/ForbiddenView.vue34
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