feat: add option to share room

fix #33
pull/63/head
sualko 2020-06-17 10:56:28 +02:00
parent bee124d19f
commit ad7eedf14b
13 changed files with 145 additions and 47 deletions

View File

@ -2,17 +2,29 @@
namespace OCA\BigBlueButton\Controller;
use OCA\BigBlueButton\Service\RoomService;
use OCA\BigBlueButton\Permission;
use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller;
use OCA\BigBlueButton\Service\RoomService;
use OCP\IGroupManager;
use OCP\IUserManager;
class RoomController extends Controller
{
/** @var RoomService */
private $service;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var Permission */
private $permission;
/** @var string */
private $userId;
@ -22,10 +34,16 @@ class RoomController extends Controller
$appName,
IRequest $request,
RoomService $service,
IUserManager $userManager,
IGroupManager $groupManager,
Permission $permission,
$userId
) {
parent::__construct($appName, $request);
$this->service = $service;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->permission = $permission;
$this->userId = $userId;
}
@ -34,17 +52,10 @@ class RoomController extends Controller
*/
public function index(): DataResponse
{
return new DataResponse($this->service->findAll($this->userId));
}
$user = $this->userManager->get($this->userId);
$groupIds = $this->groupManager->getUserGroupIds($user);
/**
* @NoAdminRequired
*/
public function show(int $id): DataResponse
{
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
return new DataResponse($this->service->findAll($this->userId, $groupIds));
}
/**
@ -77,8 +88,14 @@ class RoomController extends Controller
string $access,
bool $everyoneIsModerator
): DataResponse {
$room = $this->service->find($id);
if (!$this->permission->isAdmin($room, $this->userId)) {
return new DataResponse(null, Http::STATUS_FORBIDDEN);
}
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $everyoneIsModerator, $access) {
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $this->userId);
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator);
});
}
@ -87,8 +104,15 @@ class RoomController extends Controller
*/
public function destroy(int $id): DataResponse
{
$room = $this->service->find($id);
if (!$this->permission->isAdmin($room, $this->userId)) {
return new DataResponse(null, Http::STATUS_FORBIDDEN);
}
return $this->handleNotFound(function () use ($id) {
return $this->service->delete($id, $this->userId);
//@TODO delete shares
return $this->service->delete($id);
});
}
}

View File

@ -143,9 +143,9 @@ class RoomShareController extends Controller
private function isUserAllowed(int $roomId): bool
{
try {
$room = $this->roomService->find($roomId, $this->userId);
$room = $this->roomService->find($roomId);
return $room !== null;
return $room->getUserId() === $this->userId;
} catch (RoomShareNotFound $e) {
return false;
}

View File

@ -3,6 +3,7 @@
namespace OCA\BigBlueButton\Controller;
use OCA\BigBlueButton\BigBlueButton\API;
use OCA\BigBlueButton\Permission;
use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
@ -18,6 +19,9 @@ class ServerController extends Controller
/** @var API */
private $server;
/** @var Permission */
private $permission;
/** @var string */
private $userId;
@ -26,12 +30,14 @@ class ServerController extends Controller
IRequest $request,
RoomService $service,
API $server,
Permission $permission,
$UserId
) {
parent::__construct($appName, $request);
$this->service = $service;
$this->server = $server;
$this->permission = $permission;
$this->userId = $UserId;
}
@ -46,7 +52,7 @@ class ServerController extends Controller
return new DataResponse([], Http::STATUS_NOT_FOUND);
}
if ($room->userId !== $this->userId) {
if (!$this->permission->isAdmin($room, $this->userId)) {
return new DataResponse([], Http::STATUS_FORBIDDEN);
}
@ -68,7 +74,7 @@ class ServerController extends Controller
return new DataResponse(false, Http::STATUS_NOT_FOUND);
}
if ($room->userId !== $this->userId) {
if (!$this->permission->isAdmin($room, $this->userId)) {
return new DataResponse(false, Http::STATUS_FORBIDDEN);
}

View File

@ -37,6 +37,7 @@ class Room extends Entity implements JsonSerializable
return [
'id' => $this->id,
'uid' => $this->uid,
'userId' => $this->userId,
'name' => $this->name,
'welcome' => $this->welcome,
'maxParticipants' => (int) $this->maxParticipants,

View File

@ -21,14 +21,13 @@ class RoomMapper extends QBMapper
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function find(int $id, string $userId): Room
public function find(int $id): Room
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('bbb_rooms')
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
return $this->findEntity($qb);
}
@ -43,22 +42,39 @@ class RoomMapper extends QBMapper
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('bbb_rooms')
->from($this->tableName)
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)));
return $this->findEntity($qb);
}
/**
* @param int $userId
* @param array $groupIds
* @return array
*/
public function findAll(string $userId): array
public function findAll(string $userId, array $groupIds): array
{
/* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder();
$qb->select('*')
->from('bbb_rooms')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
$qb->select('r.*')
->from($this->tableName, 'r')
->leftJoin('r', 'bbb_room_shares', 's', $qb->expr()->eq('r.id', 's.room_id'))
->where(
$qb->expr()->orX(
$qb->expr()->eq('r.user_id', $qb->createNamedParameter($userId)),
$qb->expr()->andX(
$qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_USER, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('s.share_with', $qb->createNamedParameter($userId))
),
$qb->expr()->andX(
$qb->expr()->eq('s.permission', $qb->createNamedParameter(RoomShare::PERMISSION_ADMIN, IQueryBuilder::PARAM_INT)),
$qb->expr()->eq('s.share_type', $qb->createNamedParameter(RoomShare::SHARE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
$qb->expr()->in('s.share_with', $groupIds)
)
)
)
->groupBy('r.id');
return $this->findEntities($qb);
}
}

View File

@ -8,6 +8,7 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCA\BigBlueButton\Db\Room;
use OCA\BigBlueButton\Db\RoomMapper;
use OCA\BigBlueButton\NoPermissionException;
class RoomService
{
@ -20,9 +21,9 @@ class RoomService
$this->mapper = $mapper;
}
public function findAll(string $userId): array
public function findAll(string $userId, array $groupIds): array
{
return $this->mapper->findAll($userId);
return $this->mapper->findAll($userId, $groupIds);
}
private function handleException(Exception $e): void
@ -35,10 +36,10 @@ class RoomService
}
}
public function find($id, $userId)
public function find($id): Room
{
try {
return $this->mapper->find($id, $userId);
return $this->mapper->find($id);
// in order to be able to plug in different storage backends like files
// for instance it is a good idea to turn storage related exceptions
@ -75,10 +76,10 @@ class RoomService
return $this->mapper->insert($room);
}
public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator, $userId)
public function update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator)
{
try {
$room = $this->mapper->find($id, $userId);
$room = $this->mapper->find($id);
if ($room->access !== $access) {
$room->setPassword($access === Room::ACCESS_PASSWORD ? $this->humanReadableRandom(8) : null);
@ -90,7 +91,6 @@ class RoomService
$room->setRecord($record);
$room->setAccess($access);
$room->setEveryoneIsModerator($everyoneIsModerator);
$room->setUserId($userId);
return $this->mapper->update($room);
} catch (Exception $e) {
@ -98,10 +98,10 @@ class RoomService
}
}
public function delete($id, $userId)
public function delete($id)
{
try {
$room = $this->mapper->find($id, $userId);
$room = $this->mapper->find($id);
$this->mapper->delete($room);
return $room;
} catch (Exception $e) {

View File

@ -21,6 +21,8 @@ class RoomShareControllerTest extends TestCase
private $userManager;
private $controller;
private $userId = 'user_foo';
public function setUp(): void
{
parent::setUp();
@ -36,7 +38,7 @@ class RoomShareControllerTest extends TestCase
$this->service,
$this->userManager,
$this->roomService,
'user_foo'
$this->userId
);
}
@ -55,10 +57,14 @@ class RoomShareControllerTest extends TestCase
->with('id')
->willReturn(1234);
$room = new Room();
$room->setUserId('user_bar');
$this->roomService
->expects($this->once())
->method('find')
->will($this->throwException(new RoomShareNotFound));
->with(1234)
->willReturn($room);
$response = $this->controller->index();
@ -74,10 +80,13 @@ class RoomShareControllerTest extends TestCase
->with('id')
->willReturn($roomId);
$room = new Room();
$room->setUserId($this->userId);
$this->roomService
->expects($this->once())
->method('find')
->willReturn(new Room());
->willReturn($room);
$this->service
->expects($this->once())
@ -100,10 +109,13 @@ class RoomShareControllerTest extends TestCase
->with('id')
->willReturn($roomId);
$room = new Room();
$room->setUserId($this->userId);
$this->roomService
->expects($this->once())
->method('find')
->willReturn(new Room());
->willReturn($room);
$this->service
->expects($this->once())

View File

@ -15,6 +15,7 @@ export enum Access {
export interface Room {
id: number;
uid: string;
userId: string;
name: string;
welcome: string;
maxParticipants: number;

View File

@ -15,6 +15,10 @@
margin-top: 1em;
}
.bbb-avatar {
border-radius: 50%;
}
#bbb-warning {
padding: 1em;
background-color: rgb(255, 255, 123);

View File

@ -107,6 +107,7 @@ const App: React.FC<Props> = () => {
<th onClick={() => onOrderBy('name')}>
{t('bbb', 'Name')} <SortArrow name='name' value={orderBy} direction={sortOrder} />
</th>
<th />
<th onClick={() => onOrderBy('maxParticipants')}>
{t('bbb', 'Max')} <SortArrow name='maxParticipants' value={orderBy} direction={sortOrder} />
</th>

View File

@ -158,6 +158,12 @@ const RoomRow: React.FC<Props> = (props) => {
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} />;
}
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
user: room.userId,
size: 24,
requesttoken: OC.requestToken,
});
return (
<>
<tr className={showRecordings ? 'selected-row' : ''}>
@ -175,6 +181,9 @@ const RoomRow: React.FC<Props> = (props) => {
<td className="name">
{edit('name')}
</td>
<td>
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
</td>
<td className="max-participants">
{edit('maxParticipants', 'number')}
</td>
@ -193,7 +202,7 @@ const RoomRow: React.FC<Props> = (props) => {
</td>
</tr>
{showRecordings && <tr className="recordings-row">
<td colSpan={9}>
<td colSpan={10}>
<table>
<tbody>
{recordings?.map(recording => <RecordingRow key={recording.id} recording={recording} deleteRecording={deleteRecording} storeRecording={storeRecording} />)}

View File

@ -61,4 +61,12 @@
.bbb-form-shareWith {
margin-top: -1.5em;
}
.bbb-icon-unselected {
opacity: 0.2 !important;
&:hover {
opacity: 0.5 !important;
}
}

View File

@ -15,6 +15,8 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
const [recommendations, setRecommendations] = useState<ShareWith>();
const [searchResults, setSearchResults] = useState<ShareWith>();
const isOwner = room.userId === OC.currentUser;
const shares = (allShares && permission === Permission.Moderator) ?
allShares.filter(share => share.permission !== Permission.User) : allShares;
@ -31,7 +33,7 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
api.getRecommendedShareWith().then(result => setRecommendations(result));
}, []);
async function addRoomShare(shareWith: string, shareType: number, displayName: string) {
async function addRoomShare(shareWith: string, shareType: number, displayName: string, permission: Permission) {
const roomShare = await api.createRoomShare(room.id, shareType, shareWith, permission);
roomShare.shareWithDisplayName = displayName;
@ -60,6 +62,12 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
setShares((allShares ? [...allShares] : []).filter(share => share.id !== id));
}
async function toggleAdminShare(share: RoomShare) {
const newPermission = share.permission === Permission.Admin ? Permission.Moderator : Permission.Admin;
return addRoomShare(share.shareWith, share.shareType, share.shareWithDisplayName || share.shareWith, newPermission);
}
function renderSearchResults(options: ShareWith) {
return (
<ul className="bbb-selection">
@ -67,7 +75,7 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
...options.users.filter(user => !sharedUserIds.includes(user.value.shareWith)),
...options.groups.filter(group => !sharedGroupIds.includes(group.value.shareWith)),
].map(option => {
return (<li key={option.value.shareWith} onClick={() => addRoomShare(option.value.shareWith, option.value.shareType, option.label)}>
return (<li key={option.value.shareWith} onClick={() => addRoomShare(option.value.shareWith, option.value.shareType, option.label, permission)}>
{option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
</li>);
})}
@ -103,9 +111,16 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
{share.shareType === ShareType.Group && <span className="icon-group-white"></span>}
</div>
<div className="bbb-shareWith__item__label">
<h5>{displayName}{(share.permission === Permission.Moderator && permission === Permission.User) ? ` (${t('bbb', 'moderator')})` : ''}</h5>
<h5>{displayName}
{(share.permission === Permission.Moderator && permission === Permission.User) && ` (${t('bbb', 'moderator')})`}
{(share.permission === Permission.Admin) && ` (${t('bbb', 'admin')})`}</h5>
</div>
{share.id > -1 && <div className="bbb-shareWith__item__action">
{(share.id > -1 && permission === Permission.Moderator && isOwner) && <div className="bbb-shareWith__item__action">
<a className={`icon icon-shared icon-visible ${share.permission === Permission.Admin ? 'bbb-icon-selected' : 'bbb-icon-unselected'}`}
onClick={ev => {ev.preventDefault(); toggleAdminShare(share);}}
title={t('bbb', 'Share')} />
</div>}
{(share.id > -1 && isOwner) && <div className="bbb-shareWith__item__action">
<a className="icon icon-delete icon-visible"
onClick={ev => {ev.preventDefault(); deleteRoomShare(share.id);}}
title={t('bbb', 'Delete')} />
@ -124,13 +139,14 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
{shares ? renderShares(shares) : loading}
<div className="bbb-selection-container">
<input
{isOwner ? <input
type="text"
value={search}
onChange={ev => setSearch(ev.currentTarget.value)}
onFocus={() => setFocus(true)}
onBlur={() => setTimeout(() => setFocus(false), 100)}
placeholder={t('bbb', 'Name, group, ...')} />
placeholder={t('bbb', 'Name, group, ...')} /> :
<em><span className="icon icon-details icon-visible"></span> {t('bbb', 'You are not allowed to change this option, because this room is shared with you.')}</em>}
{hasFocus && (searchResults ? renderSearchResults(searchResults) : (recommendations ? renderSearchResults(recommendations) : loading))}
</div>
</>