diff --git a/app/assets/locales/en.json b/app/assets/locales/en.json index 547eaa4619..f773bf7c3e 100644 --- a/app/assets/locales/en.json +++ b/app/assets/locales/en.json @@ -14,7 +14,7 @@ "report": "Report", "share": "Share", "cancel": "Cancel", - "reset": "Reset", + "reset": "Reset", "close": "Close", "delete": "Delete", "copy": "Copy Join Link", @@ -143,7 +143,10 @@ "add_some_users": "Time to add some users!", "add_some_users_description": "To add new users, click the button below and search or select the users you want to share this room with.", "delete_shared_access": "Delete Shared Access", - "are_you_sure_delete_shared_access": "Are you sure you want to delete this Shared Access?" + "are_you_sure_delete_shared_access": "Are you sure you want to delete this Shared Access?", + "transfer_ownership": "Transfer Ownership", + "transfer_room_ownership": "Transfer Room Ownership", + "transfer_ownership_warning": "Warning: You will permanently lose ownership of this room and all management rights. This action cannot be undone." }, "settings": { "settings": "Settings", @@ -329,14 +332,14 @@ "default_role_description": "The default role to be assigned to newly created users", "registration_method": "Registration Method", "registration_method_description": "Change the way that users register to the website", - "registration_methods" : { + "registration_methods": { "open": "Open Registration", "invite": "Join by Invitation", "approval": "Approve/Decline" }, "allowed_domains": "Allowed Email Domains", "allowed_domains_signup_description": "Allow specific email domains to sign up. Format must be: @test.com,domain.com", - "enter_allowed_domains_rule" : "Enter the allowed domains" + "enter_allowed_domains_rule": "Enter the allowed domains" } }, "room_configuration": { @@ -427,7 +430,8 @@ "access_code_deleted": "The access code has been deleted.", "copied_meeting_url": "The meeting URL has been copied. The link can be used to join the meeting.", "copied_viewer_code": "The viewer access code has been copied.", - "copied_moderator_code": "The moderator access code has been copied." + "copied_moderator_code": "The moderator access code has been copied.", + "ownership_transferred": "Room ownership has been transferred." }, "site_settings": { "site_setting_updated": "The site setting has been updated.", @@ -575,8 +579,8 @@ "room_join": { "fields": { "name": { - "label": "Name", - "placeholder": "Enter your name" + "label": "Name", + "placeholder": "Enter your name" }, "access_code": { "label": "Access Code", @@ -732,4 +736,4 @@ } } } -} +} \ No newline at end of file diff --git a/app/controllers/api/v1/rooms_controller.rb b/app/controllers/api/v1/rooms_controller.rb index d777f1334f..1e45a96146 100644 --- a/app/controllers/api/v1/rooms_controller.rb +++ b/app/controllers/api/v1/rooms_controller.rb @@ -21,7 +21,7 @@ module V1 class RoomsController < ApiController skip_before_action :ensure_authenticated, only: %i[public_show public_recordings] - before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show public_recordings] + before_action :find_room, only: %i[show update destroy recordings recordings_processing purge_presentation public_show public_recordings transfer_ownership] before_action only: %i[create] do ensure_authorized('CreateRoom') @@ -32,7 +32,7 @@ class RoomsController < ApiController before_action only: %i[show update recordings recordings_processing purge_presentation] do ensure_authorized(%w[ManageRooms SharedRoom], friendly_id: params[:friendly_id]) end - before_action only: %i[destroy] do + before_action only: %i[destroy transfer_ownership] do ensure_authorized('ManageRooms', friendly_id: params[:friendly_id]) end @@ -153,6 +153,20 @@ def recordings_processing render_data data: @room.recordings_processing, status: :ok end + # POST /api/v1/rooms/:friendly_id/transfer_ownership.json + # Transfers room ownership to a different user + def transfer_ownership + new_owner = User.with_provider(current_provider).find_by(id: params[:new_owner_id]) + return render_error status: :not_found unless new_owner + return render_error status: :unprocessable_entity if new_owner.id == @room.user_id + + if @room.update(user_id: new_owner.id) + render_data status: :ok + else + render_error errors: @room.errors.to_a, status: :bad_request + end + end + private def find_room diff --git a/app/controllers/api/v1/shared_accesses_controller.rb b/app/controllers/api/v1/shared_accesses_controller.rb index c8260bd0bd..f0c2a320e6 100644 --- a/app/controllers/api/v1/shared_accesses_controller.rb +++ b/app/controllers/api/v1/shared_accesses_controller.rb @@ -21,7 +21,7 @@ module V1 class SharedAccessesController < ApiController before_action :find_room - before_action only: %i[create destroy shareable_users] do + before_action only: %i[create destroy shareable_users transferable_users] do ensure_authorized('ManageRooms', friendly_id: params[:friendly_id]) end before_action only: %i[show unshare_room] do @@ -82,6 +82,18 @@ def shareable_users render_data data: shareable_users, serializer: SharedAccessSerializer, status: :ok end + # GET /api/v1/shared_accesses/friendly_id/transferable_users.json + # Returns a list of users who can receive room ownership + def transferable_users + return render_data data: [], status: :ok unless params[:search].present? && params[:search].length >= 3 + + users = User.with_attached_avatar + .with_provider(current_provider) + .where.not(id: @room.user_id) + .shared_access_search(params[:search]) + render_data data: users, serializer: SharedAccessSerializer, status: :ok + end + private def find_room diff --git a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx index 9d8d9285f0..c58e94bff7 100644 --- a/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx +++ b/app/javascript/components/rooms/room/room_settings/RoomSettings.jsx @@ -26,6 +26,7 @@ import useDeleteRoom from '../../../../hooks/mutations/rooms/useDeleteRoom'; import RoomSettingsRow from './RoomSettingsRow'; import Modal from '../../../shared_components/modals/Modal'; import DeleteRoomForm from '../forms/DeleteRoomForm'; +import TransferOwnershipForm from '../shared_access/forms/TransferOwnershipForm'; import useRoomConfigs from '../../../../hooks/queries/rooms/useRoomConfigs'; import AccessCodeRow from './AccessCodeRow'; import useUpdateRoomSetting from '../../../../hooks/mutations/room_settings/useUpdateRoomSetting'; @@ -151,16 +152,31 @@ export default function RoomSettings() { { (!room.shared || currentUser?.permissions?.ManageRooms === 'true') && ( - {t('room.delete_room')} - - )} - body={} - /> + <> + {t('room.shared_access.transfer_ownership')} + + )} + title={t('room.shared_access.transfer_room_ownership')} + body={} + size="lg" + id="transfer-ownership-modal" + /> + {t('room.delete_room')} + + )} + body={} + /> + ) } diff --git a/app/javascript/components/rooms/room/shared_access/SharedAccess.jsx b/app/javascript/components/rooms/room/shared_access/SharedAccess.jsx index a0d484c42d..4afdf53f50 100644 --- a/app/javascript/components/rooms/room/shared_access/SharedAccess.jsx +++ b/app/javascript/components/rooms/room/shared_access/SharedAccess.jsx @@ -56,7 +56,7 @@ export default function SharedAccess() { className="ms-auto" >{t('room.shared_access.add_share_access')} -)} + )} title={t('room.shared_access.share_room_access')} body={} size="lg" diff --git a/app/javascript/components/rooms/room/shared_access/forms/TransferOwnershipForm.jsx b/app/javascript/components/rooms/room/shared_access/forms/TransferOwnershipForm.jsx new file mode 100644 index 0000000000..855b93a5f8 --- /dev/null +++ b/app/javascript/components/rooms/room/shared_access/forms/TransferOwnershipForm.jsx @@ -0,0 +1,118 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +/* eslint-disable react/jsx-props-no-spreading */ + +import React, { useState } from 'react'; +import { + Alert, Button, Form, Stack, Table, +} from 'react-bootstrap'; +import PropTypes from 'prop-types'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { ExclamationTriangleIcon } from '@heroicons/react/24/outline'; +import useTransferOwnership from '../../../../../hooks/mutations/rooms/useTransferOwnership'; +import Avatar from '../../../../users/user/Avatar'; +import SearchBar from '../../../../shared_components/search/SearchBar'; +import useTransferableUsers from '../../../../../hooks/queries/shared_accesses/useTransferableUsers'; + +export default function TransferOwnershipForm({ handleClose }) { + const { t } = useTranslation(); + const { friendlyId } = useParams(); + const transferOwnership = useTransferOwnership({ friendlyId, closeModal: handleClose }); + const [searchInput, setSearchInput] = useState(); + const [selectedUserId, setSelectedUserId] = useState(null); + const { data: transferableUsers } = useTransferableUsers(friendlyId, searchInput); + + const onSubmit = (event) => { + event.preventDefault(); + if (!selectedUserId) return; + transferOwnership.mutate({ new_owner_id: selectedUserId }); + }; + + return ( +
+ + + {t('room.shared_access.transfer_ownership_warning')} + + +
+
+ + + + + + + + { + (() => { + if (searchInput?.length >= 3 && transferableUsers?.length) { + return ( + transferableUsers.map((user) => ( + + + + ))); + } if (searchInput?.length >= 3) { + return (); + } + return (); + })() + } + +
{ t('user.name') }
+ + + setSelectedUserId(user.id)} + /> + + {user.name} + + +
{ t('user.no_user_found') }
{ t('user.type_three_characters') }
+
+ + + + +
+
+ ); +} + +TransferOwnershipForm.propTypes = { + handleClose: PropTypes.func, +}; + +TransferOwnershipForm.defaultProps = { + handleClose: () => { }, +}; diff --git a/app/javascript/hooks/mutations/rooms/useTransferOwnership.jsx b/app/javascript/hooks/mutations/rooms/useTransferOwnership.jsx new file mode 100644 index 0000000000..83ff418721 --- /dev/null +++ b/app/javascript/hooks/mutations/rooms/useTransferOwnership.jsx @@ -0,0 +1,42 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useMutation, useQueryClient } from 'react-query'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; +import axios from '../../../helpers/Axios'; + +export default function useTransferOwnership({ friendlyId, closeModal }) { + const { t } = useTranslation(); + const navigate = useNavigate(); + const queryClient = useQueryClient(); + + return useMutation( + (data) => axios.post(`/rooms/${friendlyId}/transfer_ownership.json`, { new_owner_id: data.new_owner_id }), + { + onSuccess: () => { + closeModal(); + queryClient.invalidateQueries('getRooms'); + navigate('/rooms'); + toast.success(t('toast.success.room.ownership_transferred')); + }, + onError: () => { + toast.error(t('toast.error.problem_completing_action')); + }, + }, + ); +} diff --git a/app/javascript/hooks/queries/shared_accesses/useTransferableUsers.jsx b/app/javascript/hooks/queries/shared_accesses/useTransferableUsers.jsx new file mode 100644 index 0000000000..94cebd1a41 --- /dev/null +++ b/app/javascript/hooks/queries/shared_accesses/useTransferableUsers.jsx @@ -0,0 +1,33 @@ +// BigBlueButton open source conferencing system - http://www.bigbluebutton.org/. +// +// Copyright (c) 2022 BigBlueButton Inc. and by respective authors (see below). +// +// This program is free software; you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation; either version 3.0 of the License, or (at your option) any later +// version. +// +// Greenlight 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 Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License along +// with Greenlight; if not, see . + +import { useQuery } from 'react-query'; +import axios from '../../../helpers/Axios'; + +export default function useTransferableUsers(friendlyId, input) { + const params = { + search: input, + }; + + return useQuery( + ['getTransferableUsers', { ...params }], + () => axios.get(`/shared_accesses/${friendlyId}/transferable_users.json`, { params }).then((resp) => resp.data.data), + { + keepPreviousData: true, + enabled: input?.length >= 3, + }, + ); +} diff --git a/config/routes.rb b/config/routes.rb index c30f4fd5d1..e5cdb4ddde 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ get '/recordings_processing', to: 'rooms#recordings_processing' get '/public', to: 'rooms#public_show' delete :purge_presentation + post '/transfer_ownership', to: 'rooms#transfer_ownership' end end resources :meetings, only: %i[], param: :friendly_id do @@ -69,6 +70,7 @@ resources :shared_accesses, only: %i[create show destroy], param: :friendly_id do member do get '/shareable_users', to: 'shared_accesses#shareable_users' + get '/transferable_users', to: 'shared_accesses#transferable_users' post '/unshare_room', to: 'shared_accesses#unshare_room' end end