Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions app/assets/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"report": "Report",
"share": "Share",
"cancel": "Cancel",
"reset": "Reset",
"reset": "Reset",
"close": "Close",
"delete": "Delete",
"copy": "Copy Join Link",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -732,4 +736,4 @@
}
}
}
}
}
18 changes: 16 additions & 2 deletions app/controllers/api/v1/rooms_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
14 changes: 13 additions & 1 deletion app/controllers/api/v1/shared_accesses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 26 additions & 10 deletions app/javascript/components/rooms/room/room_settings/RoomSettings.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -151,16 +152,31 @@ export default function RoomSettings() {
{
(!room.shared || currentUser?.permissions?.ManageRooms === 'true')
&& (
<Modal
modalButton={(
<Button
variant="delete"
className="mt-1 mx-2 float-end"
>{t('room.delete_room')}
</Button>
)}
body={<DeleteRoomForm mutation={deleteMutationWrapper} />}
/>
<>
<Modal
modalButton={(
<Button
variant="brand-outline"
className="mt-1 mx-2 float-end"
>{t('room.shared_access.transfer_ownership')}
</Button>
)}
title={t('room.shared_access.transfer_room_ownership')}
body={<TransferOwnershipForm />}
size="lg"
id="transfer-ownership-modal"
/>
<Modal
modalButton={(
<Button
variant="delete"
className="mt-1 mx-2 float-end"
>{t('room.delete_room')}
</Button>
)}
body={<DeleteRoomForm mutation={deleteMutationWrapper} />}
/>
</>
)
}
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function SharedAccess() {
className="ms-auto"
>{t('room.shared_access.add_share_access')}
</Button>
)}
)}
title={t('room.shared_access.share_room_access')}
body={<SharedAccessForm />}
size="lg"
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

/* 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 (
<div id="transfer-ownership-form">
<Alert variant="danger" className="d-flex align-items-start gap-2">
<ExclamationTriangleIcon className="hi-s flex-shrink-0 mt-1" />
<span>{t('room.shared_access.transfer_ownership_warning')}</span>
</Alert>
<SearchBar searchInput={searchInput} setSearchInput={setSearchInput} />
<Form onSubmit={onSubmit}>
<div className="table-scrollbar-wrapper">
<Table hover responsive className="text-secondary my-3">
<thead>
<tr className="text-muted small">
<th className="fw-normal">{ t('user.name') }</th>
</tr>
</thead>
<tbody className="border-top-0">
{
(() => {
if (searchInput?.length >= 3 && transferableUsers?.length) {
return (
transferableUsers.map((user) => (
<tr
key={user.id}
className="align-middle"
>
<td>
<Stack direction="horizontal" className="py-2">
<Form.Label className="w-100 mb-0 text-brand">
<Form.Check
id={`${user.id}-radio`}
type="radio"
name="transfer-owner"
value={user.id}
className="d-inline-block"
checked={selectedUserId === user.id}
onChange={() => setSelectedUserId(user.id)}
/>
<Avatar avatar={user.avatar} size="small" className="d-inline-block px-3" />
{user.name}
</Form.Label>
</Stack>
</td>
</tr>
)));
} if (searchInput?.length >= 3) {

Check failure on line 90 in app/javascript/components/rooms/room/shared_access/forms/TransferOwnershipForm.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Move this "if" to a new line or add the missing "else".

See more on https://sonarcloud.io/project/issues?id=bigbluebutton_greenlight&issues=AZ1EIiQVnGtvNeRXeJyg&open=AZ1EIiQVnGtvNeRXeJyg&pullRequest=6243
return (<tr className="fw-bold"><td>{ t('user.no_user_found') }</td><td /></tr>);
}
return (<tr className="fw-bold"><td colSpan="2">{ t('user.type_three_characters') }</td></tr>);
})()
}
</tbody>
</Table>
</div>
<Stack id="transfer-ownership-modal-buttons" className="mt-3" direction="horizontal" gap={1}>
<Button variant="neutral" className="ms-auto" onClick={handleClose}>
{ t('close') }
</Button>
<Button variant="danger" type="submit" disabled={!selectedUserId}>
{ t('room.shared_access.transfer_ownership') }
</Button>
</Stack>
</Form>
</div>
);
}

TransferOwnershipForm.propTypes = {
handleClose: PropTypes.func,
};

TransferOwnershipForm.defaultProps = {
handleClose: () => { },
};
42 changes: 42 additions & 0 deletions app/javascript/hooks/mutations/rooms/useTransferOwnership.jsx
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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'));
},
},
);
}
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.

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,
},
);
}
Loading