mirror of https://github.com/sualko/cloud_bbb
parent
cdf16960c4
commit
d6abf23792
|
@ -4,6 +4,7 @@ return [
|
|||
'resources' => [
|
||||
'room' => ['url' => '/rooms'],
|
||||
'roomShare' => ['url' => '/roomShares'],
|
||||
'restriction' => ['url' => '/restrictions'],
|
||||
],
|
||||
'routes' => [
|
||||
['name' => 'page#index', 'url' => '/', 'verb' => 'GET'],
|
||||
|
@ -12,5 +13,6 @@ return [
|
|||
['name' => 'server#version', 'url' => '/server/version', 'verb' => 'GET'],
|
||||
['name' => 'server#delete_record', 'url' => '/server/record/{recordId}', 'verb' => 'DELETE'],
|
||||
['name' => 'join#index', 'url' => '/b/{token}', 'verb' => 'GET'],
|
||||
['name' => 'restriction#user', 'url' => '/restrictions/user', 'verb' => 'GET'],
|
||||
]
|
||||
];
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Controller;
|
||||
|
||||
use OCA\BigBlueButton\Db\Restriction;
|
||||
use OCP\IRequest;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUserManager;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Controller;
|
||||
|
||||
use OCA\BigBlueButton\Service\RestrictionService;
|
||||
|
||||
class RestrictionController extends Controller {
|
||||
/** @var RestrictionService */
|
||||
private $service;
|
||||
|
||||
/** @var string */
|
||||
private $userId;
|
||||
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
|
||||
/** @var IGroupManager */
|
||||
private $groupManager;
|
||||
|
||||
use Errors;
|
||||
|
||||
public function __construct(
|
||||
$appName,
|
||||
IRequest $request,
|
||||
RestrictionService $service,
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
$userId
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
$this->service = $service;
|
||||
$this->userManager = $userManager;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*/
|
||||
public function user(): DataResponse {
|
||||
$user = $this->userManager->get($this->userId);
|
||||
$groupIds = $this->groupManager->getUserGroupIds($user);
|
||||
|
||||
return new DataResponse($this->service->findByGroupIds($groupIds));
|
||||
}
|
||||
|
||||
public function index(): DataResponse {
|
||||
$restrictions = $this->service->findAll();
|
||||
|
||||
if (!$this->service->existsByGroupId(Restriction::ALL_ID)) {
|
||||
$defaultRestriction = new Restriction();
|
||||
$defaultRestriction->setGroupId('');
|
||||
|
||||
$restrictions[] = $defaultRestriction;
|
||||
}
|
||||
|
||||
return new DataResponse($restrictions);
|
||||
}
|
||||
|
||||
public function create(
|
||||
string $groupId
|
||||
): DataResponse {
|
||||
if ($this->service->existsByGroupId($groupId)) {
|
||||
return new DataResponse(null, Http::STATUS_CONFLICT);
|
||||
}
|
||||
|
||||
return new DataResponse($this->service->create(
|
||||
$groupId
|
||||
));
|
||||
}
|
||||
|
||||
public function update(
|
||||
int $id,
|
||||
string $groupId,
|
||||
int $maxRooms,
|
||||
array $roomTypes,
|
||||
int $maxParticipants,
|
||||
bool $allowRecording
|
||||
): DataResponse {
|
||||
return $this->handleNotFound(function () use (
|
||||
$id,
|
||||
$groupId,
|
||||
$maxRooms,
|
||||
$roomTypes,
|
||||
$maxParticipants,
|
||||
$allowRecording) {
|
||||
return $this->service->update(
|
||||
$id,
|
||||
$groupId,
|
||||
$maxRooms,
|
||||
$roomTypes,
|
||||
$maxParticipants,
|
||||
$allowRecording
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public function destroy(int $id): DataResponse {
|
||||
return $this->handleNotFound(function () use ($id) {
|
||||
$roomShare = $this->service->find($id);
|
||||
|
||||
return $this->service->delete($id);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ namespace OCA\BigBlueButton\Controller;
|
|||
|
||||
use OCA\BigBlueButton\Service\RoomService;
|
||||
use OCA\BigBlueButton\Permission;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCP\IRequest;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
|
@ -63,13 +64,34 @@ class RoomController extends Controller {
|
|||
string $name,
|
||||
string $welcome,
|
||||
int $maxParticipants,
|
||||
bool $record
|
||||
bool $record,
|
||||
string $access
|
||||
): DataResponse {
|
||||
if (!$this->permission->isAllowedToCreateRoom($this->userId)) {
|
||||
return new DataResponse(null, Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$restriction = $this->permission->getRestriction($this->userId);
|
||||
|
||||
if ($restriction->getMaxParticipants() > -1 && ($maxParticipants > $restriction->getMaxParticipants() || $maxParticipants <= 0)) {
|
||||
return new DataResponse('Max participants limit exceeded.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$restriction->getAllowRecording() && $record) {
|
||||
return new DataResponse('Not allowed to enable recordings.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$disabledRoomTypes = \json_decode($restriction->getRoomTypes());
|
||||
if (in_array($access, $disabledRoomTypes) || !in_array($access, Room::ACCESS)) {
|
||||
return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return new DataResponse($this->service->create(
|
||||
$name,
|
||||
$welcome,
|
||||
$maxParticipants,
|
||||
$record,
|
||||
$access,
|
||||
$this->userId
|
||||
));
|
||||
}
|
||||
|
@ -92,6 +114,21 @@ class RoomController extends Controller {
|
|||
return new DataResponse(null, Http::STATUS_FORBIDDEN);
|
||||
}
|
||||
|
||||
$restriction = $this->permission->getRestriction($this->userId);
|
||||
|
||||
if ($restriction->getMaxParticipants() > -1 && $maxParticipants !== $room->getMaxParticipants() && ($maxParticipants > $restriction->getMaxParticipants() || $maxParticipants <= 0)) {
|
||||
return new DataResponse('Max participants limit exceeded.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (!$restriction->getAllowRecording() && $record !== $room->getRecord()) {
|
||||
return new DataResponse('Not allowed to enable recordings.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
$disabledRoomTypes = \json_decode($restriction->getRoomTypes());
|
||||
if ((in_array($access, $disabledRoomTypes) && $access !== $room->getAccess()) || !in_array($access, Room::ACCESS)) {
|
||||
return new DataResponse('Access type not allowed.', Http::STATUS_BAD_REQUEST);
|
||||
}
|
||||
|
||||
return $this->handleNotFound(function () use ($id, $name, $welcome, $maxParticipants, $record, $everyoneIsModerator, $access) {
|
||||
return $this->service->update($id, $name, $welcome, $maxParticipants, $record, $access, $everyoneIsModerator);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Db;
|
||||
|
||||
use JsonSerializable;
|
||||
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
|
||||
/**
|
||||
* @method int getRoomId()
|
||||
* @method int getMaxRooms()
|
||||
* @method string getRoomTypes()
|
||||
* @method int getMaxParticipants()
|
||||
* @method bool getAllowRecording()
|
||||
* @method void setRoomId(string $id)
|
||||
* @method void setMaxRooms(int $number)
|
||||
* @method void setMaxParticipants(int $number)
|
||||
* @method void setAllowRecording(bool $allow)
|
||||
*/
|
||||
class Restriction extends Entity implements JsonSerializable {
|
||||
public const ALL_ID = '';
|
||||
|
||||
protected $groupId;
|
||||
protected $maxRooms = -1;
|
||||
protected $roomTypes = '[]';
|
||||
protected $maxParticipants = -1;
|
||||
protected $allowRecording = true;
|
||||
|
||||
public function __construct() {
|
||||
$this->addType('max_rooms', 'integer');
|
||||
$this->addType('max_participants', 'integer');
|
||||
$this->addType('allow_recording', 'boolean');
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'groupId' => $this->groupId,
|
||||
'maxRooms' => (int) $this->maxRooms,
|
||||
'roomTypes' => \json_decode($this->roomTypes),
|
||||
'maxParticipants' => (int) $this->maxParticipants,
|
||||
'allowRecording' => boolval($this->allowRecording),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Db;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class RestrictionMapper extends QBMapper {
|
||||
public function __construct(IDBConnection $db) {
|
||||
parent::__construct($db, 'bbb_restrictions', Restriction::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function find(int $id): Restriction {
|
||||
/* @var $qb IQueryBuilder */
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->tableName)
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function findByGroupId(string $groupId): Restriction {
|
||||
/* @var $qb IQueryBuilder */
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->tableName)
|
||||
->where($qb->expr()->eq('group_id', $qb->createNamedParameter($groupId)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Restriction>
|
||||
*/
|
||||
public function findByGroupIds(array $groupIds): array {
|
||||
/* @var $qb IQueryBuilder */
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->tableName)
|
||||
->where($qb->expr()->in('group_id', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)));
|
||||
|
||||
/** @var array<Restriction> */
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<Restriction>
|
||||
*/
|
||||
public function findAll(): array {
|
||||
/* @var $qb IQueryBuilder */
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->tableName);
|
||||
|
||||
/** @var array<Restriction> */
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
}
|
|
@ -37,6 +37,8 @@ class Room extends Entity implements JsonSerializable {
|
|||
public const ACCESS_INTERNAL = 'internal';
|
||||
public const ACCESS_INTERNAL_RESTRICTED = 'internal_restricted';
|
||||
|
||||
public const ACCESS = [self::ACCESS_PUBLIC, self::ACCESS_PASSWORD, self::ACCESS_WAITING_ROOM, self::ACCESS_INTERNAL, self::ACCESS_INTERNAL_RESTRICTED];
|
||||
|
||||
public $uid;
|
||||
public $name;
|
||||
public $attendeePassword;
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\BigBlueButton\Migration;
|
||||
|
||||
use Closure;
|
||||
use OCP\DB\ISchemaWrapper;
|
||||
use OCP\Migration\IOutput;
|
||||
use OCP\Migration\SimpleMigrationStep;
|
||||
|
||||
/**
|
||||
* Auto-generated migration step: Please modify to your needs!
|
||||
*/
|
||||
class Version000000Date20200826100844 extends SimpleMigrationStep {
|
||||
|
||||
/**
|
||||
* @param IOutput $output
|
||||
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
|
||||
* @param array $options
|
||||
* @return null|ISchemaWrapper
|
||||
*/
|
||||
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
|
||||
/** @var ISchemaWrapper $schema */
|
||||
$schema = $schemaClosure();
|
||||
|
||||
if (!$schema->hasTable('bbb_restrictions')) {
|
||||
$table = $schema->createTable('bbb_restrictions');
|
||||
$table->addColumn('id', 'integer', [
|
||||
'autoincrement' => true,
|
||||
'notnull' => true,
|
||||
]);
|
||||
$table->addColumn('group_id', 'string', [
|
||||
'unique' => true,
|
||||
'notnull' => true,
|
||||
'length' => 200,
|
||||
]);
|
||||
$table->addColumn('max_rooms', 'integer', [
|
||||
'notnull' => false,
|
||||
'default' => -1,
|
||||
]);
|
||||
$table->addColumn('room_types', 'string', [
|
||||
'notnull' => true,
|
||||
'default' => '[]',
|
||||
]);
|
||||
$table->addColumn('max_participants', 'integer', [
|
||||
'notnull' => false,
|
||||
'default' => -1,
|
||||
]);
|
||||
$table->addColumn('allow_recording', 'boolean', [
|
||||
'notnull' => true,
|
||||
'default' => true,
|
||||
]);
|
||||
|
||||
$table->setPrimaryKey(['id']);
|
||||
$table->addIndex(['group_id'], 'restrictions_group_id_index');
|
||||
}
|
||||
|
||||
return $schema;
|
||||
}
|
||||
}
|
|
@ -3,27 +3,60 @@
|
|||
namespace OCA\BigBlueButton;
|
||||
|
||||
use Closure;
|
||||
use OCA\BigBlueButton\Service\RoomService;
|
||||
use OCA\BigBlueButton\Service\RestrictionService;
|
||||
use OCA\BigBlueButton\Service\RoomShareService;
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCA\BigBlueButton\Db\RoomShare;
|
||||
use OCA\BigBlueButton\Db\Restriction;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IGroupManager;
|
||||
|
||||
class Permission {
|
||||
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
|
||||
/** @var IGroupManager */
|
||||
private $groupManager;
|
||||
|
||||
/** @var RoomService */
|
||||
private $roomService;
|
||||
|
||||
/** @var RestrictionService */
|
||||
private $restrictionService;
|
||||
|
||||
/** @var RoomShareService */
|
||||
private $roomShareService;
|
||||
|
||||
public function __construct(
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
RoomService $roomService,
|
||||
RestrictionService $restrictionService,
|
||||
RoomShareService $roomShareService
|
||||
) {
|
||||
$this->userManager = $userManager;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->roomService = $roomService;
|
||||
$this->restrictionService = $restrictionService;
|
||||
$this->roomShareService = $roomShareService;
|
||||
}
|
||||
|
||||
public function getRestriction(string $uid): Restriction {
|
||||
$user = $this->userManager->get($uid);
|
||||
$groupIds = $this->groupManager->getUserGroupIds($user);
|
||||
|
||||
return $this->restrictionService->findByGroupIds($groupIds);
|
||||
}
|
||||
|
||||
public function isAllowedToCreateRoom(string $uid) {
|
||||
$numberOfCreatedRooms = count($this->roomService->findAll($uid, []));
|
||||
$restriction = $this->getRestriction($uid);
|
||||
|
||||
return $restriction->getMaxRooms() < 0 || $restriction->getMaxRooms() > $numberOfCreatedRooms;
|
||||
}
|
||||
|
||||
public function isUser(Room $room, ?string $uid) {
|
||||
return $this->hasPermission($room, $uid, function (RoomShare $share) {
|
||||
return $share->hasUserPermission();
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Service;
|
||||
|
||||
class RestrictionNotFound extends \Exception {
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
namespace OCA\BigBlueButton\Service;
|
||||
|
||||
use Exception;
|
||||
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
|
||||
use OCA\BigBlueButton\Db\Restriction;
|
||||
use OCA\BigBlueButton\Db\RestrictionMapper;
|
||||
|
||||
class RestrictionService {
|
||||
/** @var RestrictionMapper */
|
||||
private $mapper;
|
||||
|
||||
public function __construct(RestrictionMapper $mapper) {
|
||||
$this->mapper = $mapper;
|
||||
}
|
||||
|
||||
public function findAll(): array {
|
||||
return $this->mapper->findAll();
|
||||
}
|
||||
|
||||
public function existsByGroupId(string $groupId): bool {
|
||||
try {
|
||||
$this->mapper->findByGroupId($groupId);
|
||||
|
||||
return true;
|
||||
} catch (DoesNotExistException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public function findByGroupIds(array $groupIds): Restriction {
|
||||
$restrictions = $this->mapper->findByGroupIds($groupIds);
|
||||
try {
|
||||
$restriction = $this->mapper->findByGroupId(Restriction::ALL_ID);
|
||||
} catch (DoesNotExistException $e) {
|
||||
$restriction = new Restriction();
|
||||
}
|
||||
|
||||
$roomTypes = \json_decode($restriction->getRoomTypes());
|
||||
|
||||
foreach ($restrictions as $r) {
|
||||
if ($restriction->getMaxRooms() > -1 && ($r->getMaxRooms() === -1 || $restriction->getMaxRooms() < $r->getMaxRooms())) {
|
||||
$restriction->setMaxRooms($r->getMaxRooms());
|
||||
}
|
||||
|
||||
$rRoomTypes = \json_decode($r->getRoomTypes());
|
||||
if (count($rRoomTypes) > 0) {
|
||||
$roomTypes = \array_merge($roomTypes, $rRoomTypes);
|
||||
}
|
||||
|
||||
if ($restriction->getMaxParticipants() > -1 && ($r->getMaxParticipants() === -1 || $restriction->getMaxParticipants() < $r->getMaxParticipants())) {
|
||||
$restriction->setMaxParticipants($r->getMaxParticipants());
|
||||
}
|
||||
|
||||
if (!$restriction->getAllowRecording() && $r->getAllowRecording()) {
|
||||
$restriction->setAllowRecording($r->getAllowRecording());
|
||||
}
|
||||
}
|
||||
|
||||
$restriction->setId(0);
|
||||
$restriction->setGroupId('__cumulative');
|
||||
$restriction->setRoomTypes(\json_encode(\array_values(\array_unique($roomTypes))));
|
||||
|
||||
return $restriction;
|
||||
}
|
||||
|
||||
public function find($id): Restriction {
|
||||
try {
|
||||
return $this->mapper->find($id);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function create(string $groupId): Restriction {
|
||||
$restriction = new Restriction();
|
||||
|
||||
$restriction->setGroupId($groupId);
|
||||
|
||||
return $this->mapper->insert($restriction);
|
||||
}
|
||||
|
||||
public function update(int $id, string $groupId, int $maxRooms, array $roomTypes, int $maxParticipants, bool $allowRecording): Restriction {
|
||||
try {
|
||||
$restriction = $this->mapper->find($id);
|
||||
|
||||
$restriction->setGroupId($groupId);
|
||||
$restriction->setMaxRooms(\max($maxRooms, -1));
|
||||
$restriction->setRoomTypes(\json_encode($roomTypes));
|
||||
$restriction->setMaxParticipants(\max($maxParticipants, -1));
|
||||
$restriction->setAllowRecording($allowRecording);
|
||||
|
||||
return $this->mapper->update($restriction);
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(int $id): Restriction {
|
||||
try {
|
||||
$restriction = $this->mapper->find($id);
|
||||
$this->mapper->delete($restriction);
|
||||
|
||||
return $restriction;
|
||||
} catch (Exception $e) {
|
||||
$this->handleException($e);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleException(Exception $e): void {
|
||||
if ($e instanceof DoesNotExistException ||
|
||||
$e instanceof MultipleObjectsReturnedException) {
|
||||
throw new RestrictionNotFound($e->getMessage());
|
||||
} else {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,16 +57,17 @@ class RoomService {
|
|||
}
|
||||
}
|
||||
|
||||
public function create($name, $welcome, $maxParticipants, $record, $userId) {
|
||||
public function create($name, $welcome, $maxParticipants, $record, $access, $userId) {
|
||||
$room = new Room();
|
||||
|
||||
$room->setUid(\OC::$server->getSecureRandom()->generate(16, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setName($name);
|
||||
$room->setWelcome($welcome);
|
||||
$room->setMaxParticipants($maxParticipants);
|
||||
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||
$room->setAttendeePassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setModeratorPassword(\OC::$server->getSecureRandom()->generate(32, \OCP\Security\ISecureRandom::CHAR_HUMAN_READABLE));
|
||||
$room->setRecord($record);
|
||||
$room->setAccess($access);
|
||||
$room->setUserId($userId);
|
||||
|
||||
return $this->mapper->insert($room);
|
||||
|
@ -82,7 +83,7 @@ class RoomService {
|
|||
|
||||
$room->setName($name);
|
||||
$room->setWelcome($welcome);
|
||||
$room->setMaxParticipants($maxParticipants);
|
||||
$room->setMaxParticipants(\max($maxParticipants, 0));
|
||||
$room->setRecord($record);
|
||||
$room->setAccess($access);
|
||||
$room->setEveryoneIsModerator($everyoneIsModerator);
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
/** @var $_ array */
|
||||
|
||||
script('bbb', 'admin');
|
||||
script('bbb', 'restrictions');
|
||||
?>
|
||||
|
||||
<div id="bbb-settings" class="section">
|
||||
|
@ -19,7 +20,11 @@ script('bbb', 'admin');
|
|||
</form>
|
||||
|
||||
<p>
|
||||
<input type="checkbox" name="app.navigation" id="bbb-app-navigation" class="checkbox" value="1" <?php p($_['app.navigation']); ?>>
|
||||
<input type="checkbox" name="app.navigation" id="bbb-app-navigation" class="checkbox" value="1" <?php p($_['app.navigation']); ?> />
|
||||
<label for="bbb-app-navigation"><?php p($l->t('Show room manager in app navigation instead of settings page.')); ?></label>
|
||||
</p>
|
||||
|
||||
<h3>Restrictions</h3>
|
||||
<div id="bbb-restrictions">
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,9 +4,14 @@ namespace OCA\BigBlueButton\Tests;
|
|||
|
||||
use OCA\BigBlueButton\Db\Room;
|
||||
use OCA\BigBlueButton\Db\RoomShare;
|
||||
use OCA\BigBlueButton\Db\Restriction;
|
||||
use OCA\BigBlueButton\Permission;
|
||||
use OCA\BigBlueButton\Service\RoomService;
|
||||
use OCA\BigBlueButton\Service\RoomShareService;
|
||||
use OCA\BigBlueButton\Service\RestrictionService;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
|
@ -14,27 +19,79 @@ class PermissionTest extends TestCase {
|
|||
/** @var Permission */
|
||||
private $permission;
|
||||
|
||||
/** @var IUserManager|MockObject */
|
||||
private $userManager;
|
||||
|
||||
/** @var IGroupManager|MockObject */
|
||||
private $groupManager;
|
||||
|
||||
/** @var RoomService|MockObject */
|
||||
private $roomService;
|
||||
|
||||
/** @var RoomShareService|MockObject */
|
||||
private $roomShareService;
|
||||
|
||||
/** @var RestrictionService|MockObject */
|
||||
private $restrictionService;
|
||||
|
||||
public function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
/** @var IUserManager|MockObject */
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
/** @var IGroupManager|MockObject */
|
||||
$this->groupManager = $this->createMock(IGroupManager::class);
|
||||
|
||||
/** @var RoomService|MockObject */
|
||||
$this->roomService = $this->createMock(RoomService::class);
|
||||
|
||||
/** @var RestrictionService|MockObject */
|
||||
$this->restrictionService = $this->createMock(RestrictionService::class);
|
||||
|
||||
/** @var RoomShareService|MockObject */
|
||||
$this->roomShareService = $this->createMock(RoomShareService::class);
|
||||
|
||||
$this->permission = new Permission(
|
||||
$this->userManager,
|
||||
$this->groupManager,
|
||||
$this->roomService,
|
||||
$this->restrictionService,
|
||||
$this->roomShareService
|
||||
);
|
||||
}
|
||||
|
||||
public function testIsUserNotAllowed() {
|
||||
$this->userManager
|
||||
->expects($this->once())
|
||||
->method('get')
|
||||
->with('foobar')
|
||||
->willReturn($this->createMock(IUser::class));
|
||||
|
||||
$this->groupManager
|
||||
->expects($this->once())
|
||||
->method('getUserGroupIds')
|
||||
->willReturn([]);
|
||||
|
||||
$this->roomService
|
||||
->expects($this->once())
|
||||
->method('findAll')
|
||||
->willReturn([
|
||||
$this->createRoom(1, 'foo'),
|
||||
$this->createRoom(2, 'bar'),
|
||||
]);
|
||||
|
||||
$restriction = new Restriction();
|
||||
$restriction->setMaxRooms(2);
|
||||
|
||||
$this->restrictionService
|
||||
->expects($this->once())
|
||||
->method('findByGroupIds')
|
||||
->willReturn($restriction);
|
||||
|
||||
$this->assertFalse($this->permission->isAllowedToCreateRoom('foobar'));
|
||||
}
|
||||
|
||||
public function testIsUser() {
|
||||
$room = $this->createRoom(1, 'foo');
|
||||
|
||||
|
|
|
@ -12,6 +12,15 @@ export enum Access {
|
|||
InternalRestricted = 'internal_restricted',
|
||||
}
|
||||
|
||||
export interface Restriction {
|
||||
id: number;
|
||||
groupId: string;
|
||||
maxRooms: number;
|
||||
roomTypes: string[];
|
||||
maxParticipants: number;
|
||||
allowRecording: boolean;
|
||||
}
|
||||
|
||||
export interface Room {
|
||||
id: number;
|
||||
uid: string;
|
||||
|
@ -65,6 +74,46 @@ class Api {
|
|||
return OC.generateUrl(`apps/bbb/${endpoint}`);
|
||||
}
|
||||
|
||||
public async getRestriction(): Promise<Restriction> {
|
||||
const response = await axios.get(this.getUrl('restrictions/user'));
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async getRestrictions(): Promise<Restriction[]> {
|
||||
const response = await axios.get(this.getUrl('restrictions'));
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async createRestriction(groupId: string) {
|
||||
const response = await axios.post(this.getUrl('restrictions'), {
|
||||
groupId,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async updateRestriction(restriction: Restriction) {
|
||||
if (!restriction.id) {
|
||||
const newRestriction = await this.createRestriction(
|
||||
restriction.groupId
|
||||
);
|
||||
|
||||
restriction.id = newRestriction.id;
|
||||
}
|
||||
|
||||
const response = await axios.put(this.getUrl(`restrictions/${restriction.id}`), restriction);
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public async deleteRestriction(id: number) {
|
||||
const response = await axios.delete(this.getUrl(`restrictions/${id}`));
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
public getRoomUrl(room: Room) {
|
||||
return window.location.origin + api.getUrl(`b/${room.uid}`);
|
||||
}
|
||||
|
@ -75,12 +124,13 @@ class Api {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
public async createRoom(name: string) {
|
||||
public async createRoom(name: string, access: Access = Access.Public, maxParticipants = 0) {
|
||||
const response = await axios.post(this.getUrl('rooms'), {
|
||||
name,
|
||||
welcome: '',
|
||||
maxParticipants: 0,
|
||||
maxParticipants,
|
||||
record: false,
|
||||
access,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
|
@ -165,10 +215,11 @@ class Api {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
public async getRecommendedShareWith(): Promise<ShareWith> {
|
||||
public async getRecommendedShareWith(shareType: ShareType[] = [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP]): Promise<ShareWith> {
|
||||
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees_recommended';
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
shareType,
|
||||
itemType: 'room',
|
||||
format: 'json',
|
||||
},
|
||||
|
@ -180,12 +231,12 @@ class Api {
|
|||
};
|
||||
}
|
||||
|
||||
public async searchShareWith(search = ''): Promise<ShareWith> {
|
||||
public async searchShareWith(search = '', shareType: ShareType[] = [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP]): Promise<ShareWith> {
|
||||
const url = OC.linkToOCS('apps/files_sharing/api/v1', 1) + 'sharees';
|
||||
const response = await axios.get(url, {
|
||||
params: {
|
||||
search,
|
||||
shareType: [OC.Share.SHARE_TYPE_USER, OC.Share.SHARE_TYPE_GROUP],
|
||||
shareType,
|
||||
itemType: 'room',
|
||||
format: 'json',
|
||||
lookup: false,
|
|
@ -0,0 +1,49 @@
|
|||
import React, {useState} from 'react';
|
||||
|
||||
type Props = {
|
||||
values: string[];
|
||||
setValue: (field: string, value: string[]) => Promise<void>;
|
||||
field: string;
|
||||
options: {[key: string]: string};
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const EditableSelection: React.FC<Props> = ({ setValue, field, values: currentValues, options, placeholder }) => {
|
||||
const [active, setActive] = useState<boolean>(false);
|
||||
|
||||
currentValues = currentValues || [];
|
||||
|
||||
function onClick(ev) {
|
||||
ev.stopPropagation();
|
||||
|
||||
setActive(!active);
|
||||
}
|
||||
|
||||
function addOption(optionKey: string, selected: boolean) {
|
||||
if (selected) {
|
||||
if (currentValues.indexOf(optionKey) < 0) {
|
||||
setValue(field, [...currentValues, optionKey]);
|
||||
}
|
||||
} else {
|
||||
setValue(field, currentValues.filter(value => value !== optionKey));
|
||||
}
|
||||
}
|
||||
|
||||
return (<>
|
||||
<a className="action-rename" onClick={onClick}>{currentValues?.join(', ') || placeholder}</a>
|
||||
{active && <ul className="bbb-selection">
|
||||
{Object.keys(options).map(key => {
|
||||
const label = options[key];
|
||||
|
||||
return (
|
||||
<li key={key}>
|
||||
<input type="checkbox" id={key} className="checkbox" checked={currentValues.indexOf(key) > -1} value="1" onChange={(ev) => addOption(key, ev.target.checked)} />
|
||||
<label htmlFor={key}>{label}</label>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>}
|
||||
</>);
|
||||
};
|
||||
|
||||
export default EditableSelection;
|
|
@ -0,0 +1,17 @@
|
|||
import { Access } from './Api';
|
||||
|
||||
interface EscapeOptions {
|
||||
escape?: boolean;
|
||||
}
|
||||
|
||||
export function bt(string: string, vars?: { [key: string]: string }, count?: number, options?: EscapeOptions): string {
|
||||
return t('bbb', string, vars, count, options);
|
||||
}
|
||||
|
||||
export const AccessOptions = {
|
||||
[Access.Public]: bt('Public'),
|
||||
[Access.Password]: bt('Internal + Password protection for guests'),
|
||||
[Access.WaitingRoom]: bt('Internal + Waiting room for guests'),
|
||||
[Access.Internal]: bt('Internal'),
|
||||
[Access.InternalRestricted]: bt('Internal restricted'),
|
||||
};
|
|
@ -19,6 +19,10 @@
|
|||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#bbb-root {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -54,6 +58,10 @@
|
|||
width: 250px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 3em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
|
|||
import './App.scss';
|
||||
import RoomRow from './RoomRow';
|
||||
import { SortArrow } from './SortArrow';
|
||||
import { api, Room } from './Api';
|
||||
import { api, Room, Restriction, Access } from '../Common/Api';
|
||||
import NewRoomForm from './NewRoomForm';
|
||||
|
||||
export type SortKey = 'name' | 'welcome' | 'maxParticipants' | 'record';
|
||||
|
@ -37,16 +37,19 @@ type Props = {
|
|||
const App: React.FC<Props> = () => {
|
||||
const [areRoomsLoaded, setRoomsLoaded] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [restriction, setRestriction] = useState<Restriction>();
|
||||
const [rooms, setRooms] = useState<Room[]>([]);
|
||||
const [orderBy, setOrderBy] = useState<SortKey>('name');
|
||||
const [sortOrder, setSortOrder] = useState(SortOrder.ASC);
|
||||
|
||||
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} />);
|
||||
const rows = rooms.sort(sortRooms(orderBy, sortOrder)).map(room => <RoomRow room={room} restriction={restriction} key={room.id} updateRoom={updateRoom} deleteRoom={deleteRoom} />);
|
||||
|
||||
useEffect(() => {
|
||||
if (areRoomsLoaded) {
|
||||
return;
|
||||
}
|
||||
api.getRestriction().then(restriction => {
|
||||
setRestriction(restriction);
|
||||
}).catch(err => {
|
||||
console.warn('Could not load restriction', err);
|
||||
});
|
||||
|
||||
api.getRooms().then(rooms => {
|
||||
setRooms(rooms);
|
||||
|
@ -57,7 +60,7 @@ const App: React.FC<Props> = () => {
|
|||
}).then(() => {
|
||||
setRoomsLoaded(true);
|
||||
});
|
||||
}, [areRoomsLoaded]);
|
||||
}, []);
|
||||
|
||||
function onOrderBy(key: SortKey) {
|
||||
if (orderBy === key) {
|
||||
|
@ -72,7 +75,16 @@ const App: React.FC<Props> = () => {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return api.createRoom(name).then(room => {
|
||||
let access = Access.Public;
|
||||
|
||||
const disabledRoomTypes = restriction?.roomTypes || [];
|
||||
if (disabledRoomTypes.length > 0 && disabledRoomTypes.indexOf(access) > -1) {
|
||||
access = Object.values(Access).filter(a => disabledRoomTypes.indexOf(a) < 0)[0] as Access;
|
||||
}
|
||||
|
||||
const maxParticipants = restriction?.maxParticipants || 0;
|
||||
|
||||
return api.createRoom(name, access, maxParticipants).then(room => {
|
||||
setRooms(rooms.concat([room]));
|
||||
});
|
||||
}
|
||||
|
@ -95,6 +107,8 @@ const App: React.FC<Props> = () => {
|
|||
});
|
||||
}
|
||||
|
||||
const maxRooms = restriction?.maxRooms || 0;
|
||||
|
||||
return (
|
||||
<div id="bbb-react-root"
|
||||
onClick={() => { /* @TODO hide edit inputs */ }}>
|
||||
|
@ -131,7 +145,12 @@ const App: React.FC<Props> = () => {
|
|||
{!areRoomsLoaded && <span className="icon icon-loading-small icon-visible"></span>}
|
||||
</td>
|
||||
<td>
|
||||
<NewRoomForm addRoom={addRoom} />
|
||||
{(maxRooms > rows.length || maxRooms < 0) ?
|
||||
<NewRoomForm addRoom={addRoom} /> :
|
||||
<p className="text-muted">{maxRooms === 0 ?
|
||||
t('bbb', 'You are not permitted to create a room.') :
|
||||
t('bbb', 'You exceeded the maximum number of rooms.')
|
||||
}</p>}
|
||||
</td>
|
||||
<td colSpan={4} />
|
||||
</tr>
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Room } from './Api';
|
||||
import { Room, Restriction } from '../Common/Api';
|
||||
import EditRoomDialog from './EditRoomDialog';
|
||||
|
||||
type Props = {
|
||||
room: Room;
|
||||
restriction?: Restriction;
|
||||
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
||||
}
|
||||
|
||||
const EditRoom: React.FC<Props> = ({ room, updateProperty }) => {
|
||||
const EditRoom: React.FC<Props> = ({ room, restriction, updateProperty }) => {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
|
@ -16,7 +17,7 @@ const EditRoom: React.FC<Props> = ({ room, updateProperty }) => {
|
|||
onClick={ev => { ev.preventDefault(), setOpen(true); }}
|
||||
title={t('bbb', 'Edit')} />
|
||||
|
||||
<EditRoomDialog room={room} updateProperty={updateProperty} open={open} setOpen={setOpen} />
|
||||
<EditRoomDialog room={room} restriction={restriction} updateProperty={updateProperty} open={open} setOpen={setOpen} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Access, Room, Permission, RoomShare, api } from './Api';
|
||||
import { Access, Room, Permission, RoomShare, api, Restriction } from '../Common/Api';
|
||||
import Dialog from './Dialog';
|
||||
import ShareWith from './ShareWith';
|
||||
import { SubmitInput } from './SubmitInput';
|
||||
import { AccessOptions } from '../Common/Translation';
|
||||
|
||||
const descriptions: { [key: string]: string } = {
|
||||
name: t('bbb', 'Descriptive name of this room.'),
|
||||
|
@ -15,14 +16,18 @@ const descriptions: { [key: string]: string } = {
|
|||
|
||||
type Props = {
|
||||
room: Room;
|
||||
restriction?: Restriction;
|
||||
updateProperty: (key: string, value: string | boolean | number) => Promise<void>;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }) => {
|
||||
const EditRoomDialog: React.FC<Props> = ({ room, restriction, updateProperty, open, setOpen }) => {
|
||||
const [shares, setShares] = useState<RoomShare[]>();
|
||||
|
||||
const maxParticipantsLimit = (restriction?.maxParticipants || 0) < 0 ? undefined : restriction?.maxParticipants;
|
||||
const minParticipantsLimit = (restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) {
|
||||
return;
|
||||
|
@ -45,7 +50,7 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
|||
<h3>{label}</h3>
|
||||
</label>
|
||||
|
||||
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} />
|
||||
<SubmitInput initialValue={room[field]} type={type} name={field} onSubmitValue={value => updateProperty(field, value)} min={minParticipantsLimit} max={maxParticipantsLimit} />
|
||||
{descriptions[field] && <em>{descriptions[field]}</em>}
|
||||
</div>
|
||||
);
|
||||
|
@ -71,19 +76,20 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
|||
);
|
||||
}
|
||||
|
||||
const accessOptions = {...AccessOptions};
|
||||
for(const roomType of restriction?.roomTypes || []) {
|
||||
if (roomType !== room.access) {
|
||||
delete accessOptions[roomType];
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={() => setOpen(false)} title={t('bbb', 'Edit "{room}"', { room: room.name })}>
|
||||
{inputElement(t('bbb', 'Name'), 'name')}
|
||||
{inputElement(t('bbb', 'Welcome'), 'welcome')}
|
||||
{inputElement(t('bbb', 'Participant limit'), 'maxParticipants', 'number')}
|
||||
|
||||
{selectElement(t('bbb', 'Access'), 'access', room.access, {
|
||||
[Access.Public]: t('bbb', 'Public'),
|
||||
[Access.Password]: t('bbb', 'Internal + Password protection for guests'),
|
||||
[Access.WaitingRoom]: t('bbb', 'Internal + Waiting room for guests'),
|
||||
[Access.Internal]: t('bbb', 'Internal'),
|
||||
[Access.InternalRestricted]: t('bbb', 'Internal restricted'),
|
||||
}, (value) => {
|
||||
{selectElement(t('bbb', 'Access'), 'access', room.access, accessOptions, (value) => {
|
||||
console.log('access', value);
|
||||
updateProperty('access', value);
|
||||
})}
|
||||
|
@ -117,6 +123,7 @@ const EditRoomDialog: React.FC<Props> = ({ room, updateProperty, open, setOpen }
|
|||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={room.record}
|
||||
disabled={!restriction?.allowRecording}
|
||||
onChange={(event) => updateProperty('record', event.target.checked)} />
|
||||
<label htmlFor={`bbb-record-${room.id}`}>{t('bbb', 'Recording')}</label>
|
||||
</div>
|
||||
|
|
|
@ -6,9 +6,14 @@ type EditableValueProps = {
|
|||
setValue: (key: string, value: string | number) => Promise<void>;
|
||||
field: string;
|
||||
type: 'text' | 'number';
|
||||
options?: {
|
||||
min?: number;
|
||||
max?: number;
|
||||
disabled?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type }) => {
|
||||
const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: currentValue, type, options }) => {
|
||||
const [active, setActive] = useState<boolean>(false);
|
||||
|
||||
const submit = (value: string | number) => {
|
||||
|
@ -31,6 +36,8 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: c
|
|||
initialValue={currentValue}
|
||||
type={type}
|
||||
focus={true}
|
||||
min={options?.min}
|
||||
max={options?.max}
|
||||
/>;
|
||||
}
|
||||
|
||||
|
@ -40,6 +47,10 @@ const EditableValue: React.FC<EditableValueProps> = ({ setValue, field, value: c
|
|||
setActive(true);
|
||||
}
|
||||
|
||||
if (options?.disabled) {
|
||||
return <span>{currentValue}</span>;
|
||||
}
|
||||
|
||||
return <a className="action-rename" onClick={onClick}>{currentValue}</a>;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { Recording } from './Api';
|
||||
import { Recording } from '../Common/Api';
|
||||
|
||||
type Props = {
|
||||
recording: Recording;
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { CopyToClipboard } from 'react-copy-to-clipboard';
|
||||
import { api, Recording, Room } from './Api';
|
||||
import { api, Recording, Room, Restriction } from '../Common/Api';
|
||||
import EditRoom from './EditRoom';
|
||||
import RecordingRow from './RecordingRow';
|
||||
import EditableValue from './EditableValue';
|
||||
|
||||
type Props = {
|
||||
room: Room;
|
||||
restriction?: Restriction;
|
||||
updateRoom: (room: Room) => Promise<void>;
|
||||
deleteRoom: (id: number) => void;
|
||||
}
|
||||
|
@ -154,8 +155,8 @@ const RoomRow: React.FC<Props> = (props) => {
|
|||
);
|
||||
}
|
||||
|
||||
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} />;
|
||||
function edit(field: string, type: 'text' | 'number' = 'text', options?) {
|
||||
return <EditableValue field={field} value={room[field]} setValue={updateRoom} type={type} options={options} />;
|
||||
}
|
||||
|
||||
const avatarUrl = OC.generateUrl('/avatar/' + encodeURIComponent(room.userId) + '/' + 24, {
|
||||
|
@ -164,6 +165,9 @@ const RoomRow: React.FC<Props> = (props) => {
|
|||
requesttoken: OC.requestToken,
|
||||
});
|
||||
|
||||
const maxParticipantsLimit = props.restriction?.maxParticipants || -1;
|
||||
const minParticipantsLimit = (props.restriction?.maxParticipants || -1) < 1 ? 0 : 1;
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr className={showRecordings ? 'selected-row' : ''}>
|
||||
|
@ -185,15 +189,15 @@ const RoomRow: React.FC<Props> = (props) => {
|
|||
{room.userId !== OC.currentUser && <img src={avatarUrl} alt="Avatar" className="bbb-avatar" />}
|
||||
</td>
|
||||
<td className="max-participants bbb-shrink">
|
||||
{edit('maxParticipants', 'number')}
|
||||
{edit('maxParticipants', 'number', {min: minParticipantsLimit, max: maxParticipantsLimit < 0 ? undefined : maxParticipantsLimit})}
|
||||
</td>
|
||||
<td className="record bbb-shrink">
|
||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||
<input id={`bbb-record-${room.id}`} type="checkbox" className="checkbox" disabled={!props.restriction?.allowRecording} checked={room.record} onChange={(event) => updateRoom('record', event.target.checked)} />
|
||||
<label htmlFor={`bbb-record-${room.id}`}></label>
|
||||
</td>
|
||||
<td className="bbb-shrink"><RecordingsNumber recordings={recordings} showRecordings={showRecordings} setShowRecordings={setShowRecordings} /></td>
|
||||
<td className="edit icon-col">
|
||||
<EditRoom room={props.room} updateProperty={updateRoom} />
|
||||
<EditRoom room={props.room} restriction={props.restriction} updateProperty={updateRoom} />
|
||||
</td>
|
||||
<td className="remove icon-col">
|
||||
<a className="icon icon-delete icon-visible"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { api, ShareWith, ShareType, RoomShare, Room, Permission, ShareWithOption } from './Api';
|
||||
import { api, ShareWith, ShareType, RoomShare, Room, Permission, ShareWithOption } from '../Common/Api';
|
||||
import './ShareWith.scss';
|
||||
|
||||
type Props = {
|
||||
|
|
|
@ -40,6 +40,8 @@ export class SubmitInput extends Component<SubmitInputProps, SubmitInputState> {
|
|||
onChange={event => this.setState({value: event.currentTarget.value})}
|
||||
onBlur={() => this.props.onSubmitValue(this.state.value)}
|
||||
autoFocus={this.props.focus}
|
||||
min={this.props.min}
|
||||
max={this.props.max}
|
||||
/>
|
||||
</form>;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import '../Manager/App.scss';
|
||||
import { api, Restriction, ShareType } from '../Common/Api';
|
||||
import RestrictionRow from './RestrictionRow';
|
||||
import ShareSelection from './ShareSelection';
|
||||
|
||||
type Props = {
|
||||
|
||||
}
|
||||
|
||||
const App: React.FC<Props> = () => {
|
||||
const [areRestrictionsLoaded, setRestrictionsLoaded] = useState(false);
|
||||
const [error, setError] = useState<string>('');
|
||||
const [restrictions, setRestrictions] = useState<Restriction[]>([]);
|
||||
|
||||
const rows = restrictions.sort((a, b) => a.groupId.localeCompare(b.groupId)).map(restriction => <RestrictionRow key={restriction.id} restriction={restriction} updateRestriction={updateRestriction} deleteRestriction={deleteRestriction} />);
|
||||
|
||||
useEffect(() => {
|
||||
api.getRestrictions().then(restrictions => {
|
||||
setRestrictions(restrictions);
|
||||
}).catch((err) => {
|
||||
console.warn('Could not load restrictions', err);
|
||||
|
||||
setError(t('bbb', 'Server error'));
|
||||
}).then(() => {
|
||||
setRestrictionsLoaded(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
function addRestriction(groupId: string) {
|
||||
return api.createRestriction(groupId).then(restriction => {
|
||||
setRestrictions([...restrictions, restriction]);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRestriction(restriction: Restriction) {
|
||||
return api.updateRestriction(restriction).then(updatedRestriction => {
|
||||
setRestrictions(restrictions.map(restriction => {
|
||||
if (restriction.id === updatedRestriction.id || restriction.groupId === updatedRestriction.groupId) {
|
||||
return updatedRestriction;
|
||||
}
|
||||
|
||||
return restriction;
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRestriction(id: number) {
|
||||
api.deleteRestriction(id).then(deletedRestriction => {
|
||||
setRestrictions(restrictions.filter(restriction => restriction.id !== deletedRestriction.id));
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div id="bbb-react-root"
|
||||
onClick={() => { /* @TODO hide edit inputs */ }}>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
{t('bbb', 'Group name')}
|
||||
</th>
|
||||
<th>
|
||||
{t('bbb', 'Max. rooms')}
|
||||
</th>
|
||||
<th>
|
||||
{t('bbb', 'Forbidden room types')}
|
||||
</th>
|
||||
<th>
|
||||
{t('bbb', 'Max. participants')}
|
||||
</th>
|
||||
<th>
|
||||
{t('bbb', 'Recording')}
|
||||
</th>
|
||||
<th/>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td>
|
||||
{!areRestrictionsLoaded
|
||||
? <span className="icon icon-loading-small icon-visible"></span>
|
||||
: <ShareSelection
|
||||
placeholder={t('bbb', 'Group, ...')}
|
||||
selectShare={(share) => addRestriction(share.value.shareWith)}
|
||||
shareType={[ShareType.Group]}
|
||||
excluded={{groupIds: restrictions.map(restriction => restriction.groupId)}} /> }
|
||||
{error && <><span className="icon icon-error icon-visible"></span> {error}</>}
|
||||
</td>
|
||||
<td colSpan={4} />
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
||||
<p className="text-muted">{t('bbb', 'Restrictions do not affect existing rooms. Minus one means the value is unlimited. The least restrictive option is chosen for every user if multiple restrictions apply.')}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,77 @@
|
|||
import React, { } from 'react';
|
||||
import { Restriction } from '../Common/Api';
|
||||
import EditableValue from '../Manager/EditableValue';
|
||||
import EditableSelection from '../Common/EditableSelection';
|
||||
import { AccessOptions } from '../Common/Translation';
|
||||
|
||||
type Props = {
|
||||
restriction: Restriction;
|
||||
updateRestriction: (restriction: Restriction) => Promise<void>;
|
||||
deleteRestriction: (id: number) => void;
|
||||
}
|
||||
|
||||
|
||||
const RestrictionRoom: React.FC<Props> = (props) => {
|
||||
const restriction = props.restriction;
|
||||
|
||||
function updateRestriction(key: string, value: string | boolean | number | string[]) {
|
||||
return props.updateRestriction({
|
||||
...props.restriction,
|
||||
[key]: value,
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRow(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
|
||||
OC.dialogs.confirm(
|
||||
t('bbb', 'Are you sure you want to delete the restrictions for group "{name}"? This operation can not be undone.', { name: restriction.groupId }),
|
||||
t('bbb', 'Delete restrictions for "{name}"?', { name: restriction.groupId }),
|
||||
confirmed => {
|
||||
if (confirmed) {
|
||||
props.deleteRestriction(restriction.id);
|
||||
}
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
function edit(field: string, type: 'text' | 'number' = 'text') {
|
||||
return <EditableValue field={field} value={restriction[field]} setValue={updateRestriction} type={type} options={{min: -1}} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<tr>
|
||||
<td className="name">{restriction.groupId || t('bbb', 'All users')}</td>
|
||||
<td className="max-rooms bbb-shrink">
|
||||
{edit('maxRooms', 'number')}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<EditableSelection field="roomTypes" values={restriction.roomTypes} options={AccessOptions} setValue={updateRestriction} placeholder={t('bbb', 'No restriction')} />
|
||||
</td>
|
||||
|
||||
<td className="max-participants bbb-shrink">
|
||||
{edit('maxParticipants', 'number')}
|
||||
</td>
|
||||
|
||||
<td className="record bbb-shrink">
|
||||
<input
|
||||
id={`bbb-record-${restriction.id}`}
|
||||
type="checkbox"
|
||||
className="checkbox"
|
||||
checked={restriction.allowRecording}
|
||||
onChange={(event) => updateRestriction('allowRecording', event.target.checked)} />
|
||||
<label htmlFor={`bbb-record-${restriction.id}`}></label>
|
||||
</td>
|
||||
|
||||
<td className="remove icon-col">
|
||||
{restriction.groupId && <a className="icon icon-delete icon-visible"
|
||||
onClick={deleteRow as any}
|
||||
title={t('bbb', 'Delete')} />}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
||||
export default RestrictionRoom;
|
|
@ -0,0 +1,95 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { api, ShareWith, ShareType, ShareWithOption } from '../Common/Api';
|
||||
import './ShareWith.scss';
|
||||
|
||||
type Props = {
|
||||
selectShare: (selection: ShareWithOption) => void;
|
||||
shareType?: ShareType[];
|
||||
excluded?: {
|
||||
groupIds?: string[];
|
||||
userIds?: string[];
|
||||
};
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
const ShareSelection: React.FC<Props> = (props) => {
|
||||
const [search, setSearch] = useState<string>('');
|
||||
const [hasFocus, setFocus] = useState<boolean>(false);
|
||||
const [showSearchResults, setShowSearchResults] = useState<boolean>(false);
|
||||
const [recommendations, setRecommendations] = useState<ShareWith>();
|
||||
const [searchResults, setSearchResults] = useState<ShareWith>();
|
||||
|
||||
const shareType = props.shareType || [ShareType.User, ShareType.Group];
|
||||
const excluded = {
|
||||
userIds: props.excluded?.userIds || [],
|
||||
groupIds: props.excluded?.groupIds || [],
|
||||
};
|
||||
const placeholder = props.placeholder || t('bbb', 'Name, group, ...');
|
||||
|
||||
useEffect(() => {
|
||||
setSearchResults(undefined);
|
||||
const searchQuery = search;
|
||||
|
||||
if (!searchQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.searchShareWith(searchQuery, shareType).then(result => {
|
||||
if (searchQuery === search) {
|
||||
setSearchResults(result);
|
||||
}
|
||||
});
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
api.getRecommendedShareWith(shareType).then(result => setRecommendations(result));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => setShowSearchResults(hasFocus), 100);
|
||||
}, [hasFocus]);
|
||||
|
||||
async function selectShare(share: ShareWithOption) {
|
||||
props.selectShare(share);
|
||||
|
||||
setSearch('');
|
||||
}
|
||||
|
||||
function renderSearchResults(options: ShareWith|undefined) {
|
||||
const results = options ? [
|
||||
...options.users.filter(user => !excluded.userIds.includes(user.value.shareWith)),
|
||||
...options.groups.filter(group => !excluded.groupIds.includes(group.value.shareWith)),
|
||||
] : [];
|
||||
|
||||
const renderOption = (option: ShareWithOption) => {
|
||||
return (<li key={option.value.shareWith} className="suggestion" onClick={() => selectShare(option)}>
|
||||
{option.label}{option.value.shareType === ShareType.Group ? ` (${t('bbb', 'Group')})` : ''}
|
||||
</li>);
|
||||
};
|
||||
|
||||
return (
|
||||
<ul className="bbb-selection">
|
||||
{!options ?
|
||||
<li><span className="icon icon-loading-small icon-visible"></span> {t('bbb', 'Searching')}</li> :
|
||||
(
|
||||
(results.length === 0 && search) ? <li>{t('bbb', 'No matches')}</li> : results.map(renderOption)
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bbb-selection-container">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={ev => setSearch(ev.currentTarget.value)}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
placeholder={placeholder} />
|
||||
{showSearchResults && renderSearchResults((search && searchResults) ? searchResults : ((recommendations && !search) ? recommendations : undefined))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ShareSelection;
|
|
@ -0,0 +1,82 @@
|
|||
.bbb-shareWith {
|
||||
margin: 1em 0;
|
||||
|
||||
&__item {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-dark);
|
||||
}
|
||||
|
||||
.avatardiv {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
overflow: hidden;
|
||||
|
||||
.icon-group-white {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #a9a9a9;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
padding: 0 1em;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bbb-selection-container {
|
||||
position: relative;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.bbb-selection {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid var(--color-border-dark);
|
||||
max-height: 88px;
|
||||
overflow: auto;
|
||||
box-shadow: 0 5px 10px -5px var(--color-box-shadow);
|
||||
z-index: 100;
|
||||
|
||||
li {
|
||||
color: var(--color-text-lighter);
|
||||
padding: 0 1em;
|
||||
line-height: 44px;
|
||||
|
||||
&.suggestion {
|
||||
color: var(--color-main-text);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bbb-form-shareWith {
|
||||
margin-top: -1.5em;
|
||||
}
|
||||
|
||||
.bbb-icon-unselected {
|
||||
opacity: 0.2 !important;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.5 !important;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
'use strict';
|
||||
|
||||
import App from './App';
|
||||
import React from 'react';
|
||||
import ReactDom from 'react-dom';
|
||||
|
||||
// Enable React devtools
|
||||
window['React'] = React;
|
||||
|
||||
$(document).ready(() => {
|
||||
ReactDom.render( <App/>, document.getElementById('bbb-restrictions'));
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import {api} from './Manager/Api';
|
||||
import {api} from './Common/Api';
|
||||
import './Manager/App.scss';
|
||||
|
||||
declare const OCP: any;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import axios from '@nextcloud/axios';
|
||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router';
|
||||
import { Room } from './Manager/Api';
|
||||
import { Room } from './Common/Api';
|
||||
|
||||
declare const OCA: any;
|
||||
|
||||
|
|
|
@ -11,6 +11,9 @@ module.exports = {
|
|||
manager: [
|
||||
path.join(__dirname, 'ts', 'Manager', 'index.tsx'),
|
||||
],
|
||||
restrictions: [
|
||||
path.join(__dirname, 'ts', 'Restrictions', 'index.tsx'),
|
||||
],
|
||||
join: [
|
||||
path.join(__dirname, 'ts', 'join.ts'),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue