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; namespace OCA\BigBlueButton\Controller;
use OCA\BigBlueButton\Service\RoomService;
use OCA\BigBlueButton\Permission;
use OCP\IRequest; use OCP\IRequest;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\Controller; use OCP\AppFramework\Controller;
use OCP\IGroupManager;
use OCA\BigBlueButton\Service\RoomService; use OCP\IUserManager;
class RoomController extends Controller class RoomController extends Controller
{ {
/** @var RoomService */ /** @var RoomService */
private $service; private $service;
/** @var IUserManager */
private $userManager;
/** @var IGroupManager */
private $groupManager;
/** @var Permission */
private $permission;
/** @var string */ /** @var string */
private $userId; private $userId;
@ -22,10 +34,16 @@ class RoomController extends Controller
$appName, $appName,
IRequest $request, IRequest $request,
RoomService $service, RoomService $service,
IUserManager $userManager,
IGroupManager $groupManager,
Permission $permission,
$userId $userId
) { ) {
parent::__construct($appName, $request); parent::__construct($appName, $request);
$this->service = $service; $this->service = $service;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->permission = $permission;
$this->userId = $userId; $this->userId = $userId;
} }
@ -34,17 +52,10 @@ class RoomController extends Controller
*/ */
public function index(): DataResponse public function index(): DataResponse
{ {
return new DataResponse($this->service->findAll($this->userId)); $user = $this->userManager->get($this->userId);
} $groupIds = $this->groupManager->getUserGroupIds($user);
/** return new DataResponse($this->service->findAll($this->userId, $groupIds));
* @NoAdminRequired
*/
public function show(int $id): DataResponse
{
return $this->handleNotFound(function () use ($id) {
return $this->service->find($id, $this->userId);
});
} }
/** /**
@ -77,8 +88,14 @@ class RoomController extends Controller
string $access, string $access,
bool $everyoneIsModerator bool $everyoneIsModerator
): DataResponse { ): 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->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 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->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 private function isUserAllowed(int $roomId): bool
{ {
try { try {
$room = $this->roomService->find($roomId, $this->userId); $room = $this->roomService->find($roomId);
return $room !== null; return $room->getUserId() === $this->userId;
} catch (RoomShareNotFound $e) { } catch (RoomShareNotFound $e) {
return false; return false;
} }

View File

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

View File

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

View File

@ -21,14 +21,13 @@ class RoomMapper extends QBMapper
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException * @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
* @throws DoesNotExistException * @throws DoesNotExistException
*/ */
public function find(int $id, string $userId): Room public function find(int $id): Room
{ {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('bbb_rooms') ->from($this->tableName)
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId)));
return $this->findEntity($qb); return $this->findEntity($qb);
} }
@ -43,22 +42,39 @@ class RoomMapper extends QBMapper
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('*')
->from('bbb_rooms') ->from($this->tableName)
->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid))); ->where($qb->expr()->eq('uid', $qb->createNamedParameter($uid)));
return $this->findEntity($qb); return $this->findEntity($qb);
} }
/** /**
* @param int $userId * @param int $userId
* @param array $groupIds
* @return array * @return array
*/ */
public function findAll(string $userId): array public function findAll(string $userId, array $groupIds): array
{ {
/* @var $qb IQueryBuilder */ /* @var $qb IQueryBuilder */
$qb = $this->db->getQueryBuilder(); $qb = $this->db->getQueryBuilder();
$qb->select('*') $qb->select('r.*')
->from('bbb_rooms') ->from($this->tableName, 'r')
->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId))); ->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); return $this->findEntities($qb);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,4 +61,12 @@
.bbb-form-shareWith { .bbb-form-shareWith {
margin-top: -1.5em; 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 [recommendations, setRecommendations] = useState<ShareWith>();
const [searchResults, setSearchResults] = useState<ShareWith>(); const [searchResults, setSearchResults] = useState<ShareWith>();
const isOwner = room.userId === OC.currentUser;
const shares = (allShares && permission === Permission.Moderator) ? const shares = (allShares && permission === Permission.Moderator) ?
allShares.filter(share => share.permission !== Permission.User) : allShares; 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)); 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); const roomShare = await api.createRoomShare(room.id, shareType, shareWith, permission);
roomShare.shareWithDisplayName = displayName; 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)); 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) { function renderSearchResults(options: ShareWith) {
return ( return (
<ul className="bbb-selection"> <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.users.filter(user => !sharedUserIds.includes(user.value.shareWith)),
...options.groups.filter(group => !sharedGroupIds.includes(group.value.shareWith)), ...options.groups.filter(group => !sharedGroupIds.includes(group.value.shareWith)),
].map(option => { ].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')})` : ''} {option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
</li>); </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>} {share.shareType === ShareType.Group && <span className="icon-group-white"></span>}
</div> </div>
<div className="bbb-shareWith__item__label"> <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> </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" <a className="icon icon-delete icon-visible"
onClick={ev => {ev.preventDefault(); deleteRoomShare(share.id);}} onClick={ev => {ev.preventDefault(); deleteRoomShare(share.id);}}
title={t('bbb', 'Delete')} /> title={t('bbb', 'Delete')} />
@ -124,13 +139,14 @@ const ShareWith: React.FC<Props> = ({ room, permission, shares: allShares, setSh
{shares ? renderShares(shares) : loading} {shares ? renderShares(shares) : loading}
<div className="bbb-selection-container"> <div className="bbb-selection-container">
<input {isOwner ? <input
type="text" type="text"
value={search} value={search}
onChange={ev => setSearch(ev.currentTarget.value)} onChange={ev => setSearch(ev.currentTarget.value)}
onFocus={() => setFocus(true)} onFocus={() => setFocus(true)}
onBlur={() => setTimeout(() => setFocus(false), 100)} 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))} {hasFocus && (searchResults ? renderSearchResults(searchResults) : (recommendations ? renderSearchResults(recommendations) : loading))}
</div> </div>
</> </>