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
31 changes: 24 additions & 7 deletions app/controllers/api/v1/shared_accesses_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
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 @@ -68,22 +68,39 @@
# GET /api/v1/shared_accesses/friendly_id/shareable_users.json
# Returns a list of users with whom a room can be shared with (based on role permissions)
def shareable_users
return render_data data: [], status: :ok unless params[:search].present? && params[:search].length >= 3
return render_data data: [], status: :ok unless search_valid?

# role_id of roles that have SharedList permission set to true
role_ids = RolePermission.joins(:permission).where(permission: { name: 'SharedList' }).where(value: 'true').pluck(:role_id)

# Can't share the room if it's already shared or it's the room owner
shareable_users = User.with_attached_avatar
.with_provider(current_provider)
.where.not(id: [@room.shared_users.pluck(:id) << @room.user_id])
.where(role_id: [role_ids])
.shared_access_search(params[:search])
shareable_users = search_users
.where.not(id: @room.shared_users.pluck(:id) + [@room.user_id])

Check warning on line 78 in app/controllers/api/v1/shared_accesses_controller.rb

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace 'pluck(:id)' with the more semantic 'ids' method.

See more on https://sonarcloud.io/project/issues?id=bigbluebutton_greenlight&issues=AZ6rgjF8bL0vMjHUoMbe&open=AZ6rgjF8bL0vMjHUoMbe&pullRequest=6243
.where(role_id: role_ids)
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 search_valid?

users = search_users.where.not(id: @room.user_id)
render_data data: users, serializer: SharedAccessSerializer, status: :ok
end

private

def search_valid?
params[:search].present? && params[:search].length >= 3
end

def search_users
User.with_attached_avatar
.with_provider(current_provider)
.shared_access_search(params[:search])
end

def find_room
@room = Room.find_by(friendly_id: params[:friendly_id])
end
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
Expand Up @@ -18,15 +18,15 @@

import React, { useState } from 'react';
import {
Button, Form, Stack, Table,
Button, Form, Stack,
} from 'react-bootstrap';
import PropTypes from 'prop-types';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import useShareAccess from '../../../../../hooks/mutations/shared_accesses/useShareAccess';
import Avatar from '../../../../users/user/Avatar';
import SearchBar from '../../../../shared_components/search/SearchBar';
import useShareableUsers from '../../../../../hooks/queries/shared_accesses/useShareableUsers';
import UserSearchTable from './UserSearchTable';

export default function SharedAccessForm({ handleClose }) {
const { t } = useTranslation();
Expand Down Expand Up @@ -55,50 +55,13 @@ export default function SharedAccessForm({ handleClose }) {
<div id="shared-access-form">
<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 && shareableUsers?.length) {
return (
shareableUsers.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}-checkbox`}
type="checkbox"
value={user.id}
className="d-inline-block"
checked={selectedUsers.includes(user.id)}
onChange={() => toggleUserSelection(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) {
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>
<UserSearchTable
users={shareableUsers}
searchInput={searchInput}
inputType="checkbox"
isChecked={(userId) => selectedUsers.includes(userId)}
onChange={toggleUserSelection}
/>
<Stack id="shared-access-modal-buttons" className="mt-3" direction="horizontal" gap={1}>
<Button variant="neutral" className="ms-auto" onClick={handleClose}>
{ t('close') }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// 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,
} 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 SearchBar from '../../../../shared_components/search/SearchBar';
import useTransferableUsers from '../../../../../hooks/queries/shared_accesses/useTransferableUsers';
import UserSearchTable from './UserSearchTable';

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}>
<UserSearchTable
users={transferableUsers}
searchInput={searchInput}
inputType="radio"
inputName="transfer-owner"
isChecked={(userId) => selectedUserId === userId}
onChange={setSelectedUserId}
/>
<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: () => { },
};
Loading