Page MenuHomePhorge

D3584.1775497599.diff
No OneTemporary

Authored By
Unknown
Size
141 KB
Referenced Files
None
Subscribers
None

D3584.1775497599.diff

diff --git a/src/app/Console/Commands/Meet/RoomCreate.php b/src/app/Console/Commands/Meet/RoomCreate.php
--- a/src/app/Console/Commands/Meet/RoomCreate.php
+++ b/src/app/Console/Commands/Meet/RoomCreate.php
@@ -47,9 +47,10 @@
return 1;
}
- \App\Meet\Room::create([
+ $room = \App\Meet\Room::create([
'name' => $roomName,
- 'user_id' => $user->id
]);
+
+ $room->assignToWallet($user->wallets()->first());
}
}
diff --git a/src/app/Console/Commands/Meet/Sessions.php b/src/app/Console/Commands/Meet/Sessions.php
--- a/src/app/Console/Commands/Meet/Sessions.php
+++ b/src/app/Console/Commands/Meet/Sessions.php
@@ -49,7 +49,7 @@
foreach ($sessions as $session) {
$room = \App\Meet\Room::where('session_id', $session['roomId'])->first();
if ($room) {
- $owner = $room->owner->email;
+ $owner = $room->wallet()->owner->email;
$roomName = $room->name;
} else {
$owner = '(none)';
diff --git a/src/app/Domain.php b/src/app/Domain.php
--- a/src/app/Domain.php
+++ b/src/app/Domain.php
@@ -326,6 +326,16 @@
);
}
+ /**
+ * Returns domain's namespace (required by the EntitleableTrait).
+ *
+ * @return string|null Domain namespace
+ */
+ public function toString(): ?string
+ {
+ return $this->namespace;
+ }
+
/**
* Unsuspend this domain.
*
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -13,7 +13,7 @@
*
* @property int $cost
* @property ?string $description
- * @property \App\Domain|\App\User $entitleable The entitled object (receiver of the entitlement).
+ * @property ?object $entitleable The entitled object (receiver of the entitlement).
* @property int $entitleable_id
* @property string $entitleable_type
* @property int $fee
@@ -100,20 +100,6 @@
return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
}
- /**
- * Returns entitleable object title (e.g. email or domain name).
- *
- * @return string|null An object title/name
- */
- public function entitleableTitle(): ?string
- {
- if ($this->entitleable instanceof Domain) {
- return $this->entitleable->namespace;
- }
-
- return $this->entitleable->email;
- }
-
/**
* Simplified Entitlement/SKU information for a specified entitleable object
*
diff --git a/src/app/Handlers/Room.php b/src/app/Handlers/Room.php
new file mode 100644
--- /dev/null
+++ b/src/app/Handlers/Room.php
@@ -0,0 +1,33 @@
+<?php
+
+namespace App\Handlers;
+
+class Room extends Base
+{
+ /**
+ * The entitleable class for this handler.
+ *
+ * @return string
+ */
+ public static function entitleableClass(): string
+ {
+ return \App\Meet\Room::class;
+ }
+
+ /**
+ * SKU handler metadata.
+ *
+ * @param \App\Sku $sku The SKU object
+ *
+ * @return array
+ */
+ public static function metadata(\App\Sku $sku): array
+ {
+ $data = parent::metadata($sku);
+
+ $data['readonly'] = true;
+ $data['enabled'] = true;
+
+ return $data;
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/DomainsController.php b/src/app/Http/Controllers/API/V4/DomainsController.php
--- a/src/app/Http/Controllers/API/V4/DomainsController.php
+++ b/src/app/Http/Controllers/API/V4/DomainsController.php
@@ -165,7 +165,7 @@
return response()->json([
'status' => 'success',
- 'message' => __('app.domain-create-success'),
+ 'message' => \trans('app.domain-create-success'),
]);
}
diff --git a/src/app/Http/Controllers/API/V4/MeetController.php b/src/app/Http/Controllers/API/V4/MeetController.php
--- a/src/app/Http/Controllers/API/V4/MeetController.php
+++ b/src/app/Http/Controllers/API/V4/MeetController.php
@@ -6,46 +6,9 @@
use App\Meet\Room;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Validator;
class MeetController extends Controller
{
- /**
- * Listing of rooms that belong to the authenticated user.
- *
- * @return \Illuminate\Http\JsonResponse
- */
- public function index()
- {
- $user = Auth::guard()->user();
-
- $rooms = Room::where('user_id', $user->id)->orderBy('name')->get();
-
- if (count($rooms) == 0) {
- // Create a room for the user (with a random and unique name)
- while (true) {
- $name = strtolower(\App\Utils::randStr(3, 3, '-'));
- if (!Room::where('name', $name)->count()) {
- break;
- }
- }
-
- $room = Room::create([
- 'name' => $name,
- 'user_id' => $user->id
- ]);
-
- $rooms = collect([$room]);
- }
-
- $result = [
- 'list' => $rooms,
- 'count' => count($rooms),
- ];
-
- return response()->json($result);
- }
-
/**
* Join the room session. Each room has one owner, and the room isn't open until the owner
* joins (and effectively creates the session).
@@ -59,17 +22,17 @@
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
- if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
+ if (!$room || !($wallet = $room->wallet()) || !$wallet->owner || $wallet->owner->isDegraded(true)) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
// Check if there's still a valid meet entitlement for the room owner
- if (!$room->owner->hasSku('meet')) {
+ if (!$wallet->owner->hasSku('meet')) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
$user = Auth::guard()->user();
- $isOwner = $user && $user->id == $room->user_id;
+ $isOwner = $user && ($user->id == $wallet->owner->id || $room->shares()->where('user', $user->email)->exists());
$init = !empty(request()->input('init'));
// There's no existing session
@@ -180,69 +143,6 @@
return response()->json($response, $response_code);
}
- /**
- * Set the domain configuration.
- *
- * @param string $id Room identifier (name)
- *
- * @return \Illuminate\Http\JsonResponse|void
- */
- public function setRoomConfig($id)
- {
- $room = Room::where('name', $id)->first();
-
- // Room does not exist, or the owner is deleted
- if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
- return $this->errorResponse(404);
- }
-
- $user = Auth::guard()->user();
-
- // Only room owner can configure the room
- if ($user->id != $room->user_id) {
- return $this->errorResponse(403);
- }
-
- $input = request()->input();
- $errors = [];
-
- foreach ($input as $key => $value) {
- switch ($key) {
- case 'password':
- if ($value === null || $value === '') {
- $input[$key] = null;
- } else {
- // TODO: Do we have to validate the password in any way?
- }
- break;
-
- case 'locked':
- $input[$key] = $value ? 'true' : null;
- break;
-
- case 'nomedia':
- $input[$key] = $value ? 'true' : null;
- break;
-
- default:
- $errors[$key] = \trans('meet.room-unsupported-option-error');
- }
- }
-
- if (!empty($errors)) {
- return response()->json(['status' => 'error', 'errors' => $errors], 422);
- }
-
- if (!empty($input)) {
- $room->setSettings($input);
- }
-
- return response()->json([
- 'status' => 'success',
- 'message' => \trans('meet.room-setconfig-success'),
- ]);
- }
-
/**
* Webhook as triggered from the Meet server
*
diff --git a/src/app/Http/Controllers/API/V4/RoomsController.php b/src/app/Http/Controllers/API/V4/RoomsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/RoomsController.php
@@ -0,0 +1,278 @@
+<?php
+
+namespace App\Http\Controllers\API\V4;
+
+use App\Http\Controllers\RelationController;
+use App\Meet\Room;
+use App\Share;
+use Illuminate\Http\Request;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Validator;
+
+class RoomsController extends RelationController
+{
+ /** @var string Resource localization label */
+ protected $label = 'room';
+
+ /** @var string Resource model name */
+ protected $model = Room::class;
+
+ /** @var array Resource listing order (column names) */
+ protected $order = ['name'];
+
+ /** @var array Common object properties in the API response */
+ protected $objectProps = ['name', 'description'];
+
+
+ /**
+ * Delete a room
+ *
+ * @param string $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function destroy($id)
+ {
+ $room = $this->inputRoom($id);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $room->delete();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-delete-success"),
+ ]);
+ }
+
+ /**
+ * Listing of rooms that belong to the authenticated user.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function index()
+ {
+ $user = $this->guard()->user();
+
+ $shared = Room::whereIn('id', function ($query) use ($user) {
+ $query->select('shareable_id')
+ ->from('shares')
+ ->where('shareable_type', Room::class)
+ ->where('user', $user->email);
+ });
+
+ $rooms = $user->rooms(true)->union($shared)->orderBy('name')->get()
+ ->map(function ($room) {
+ return $this->objectToClient($room);
+ });
+
+ $result = [
+ 'list' => $rooms,
+ 'count' => count($rooms),
+ ];
+
+ return response()->json($result);
+ }
+
+ /**
+ * Set the room configuration.
+ *
+ * @param int|string $id Room identifier (or name)
+ *
+ * @return \Illuminate\Http\JsonResponse|void
+ */
+ public function setConfig($id)
+ {
+ $room = $this->inputRoom($id, Share::ADMIN);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $errors = $room->setConfig(request()->input());
+
+ if (!empty($errors)) {
+ return response()->json(['status' => 'error', 'errors' => $errors], 422);
+ }
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-setconfig-success"),
+ ]);
+ }
+
+ /**
+ * Display information of a room specified by $id.
+ *
+ * @param string $id The room to show information for.
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function show($id)
+ {
+ $room = $this->inputRoom($id, Share::READ);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $wallet = $room->wallet();
+ $user = $this->guard()->user();
+
+ $response = $this->objectToClient($room, true);
+
+ unset($response['session_id']);
+
+ $response['config'] = $room->getConfig();
+ $response['skus'] = \App\Entitlement::objectEntitlementsSummary($room);
+ $response['wallet'] = $wallet->toArray();
+
+ if ($wallet->discount) {
+ $response['wallet']['discount'] = $wallet->discount->discount;
+ $response['wallet']['discount_description'] = $wallet->discount->description;
+ }
+
+ $isOwner = $user->canDelete($room);
+ $response['canUpdate'] = $isOwner || $room->shares()->where('user', $user->email)->exists();
+ $response['canDelete'] = $isOwner;
+ $response['isOwner'] = $isOwner;
+
+ return response()->json($response);
+ }
+
+ /**
+ * Get a list of SKUs available to the room.
+ *
+ * @param int $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function skus($id)
+ {
+ $room = $this->inputRoom($id);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ return SkusController::objectSkus($room);
+ }
+
+ /**
+ * Create a new room.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function store(Request $request)
+ {
+ $user = $this->guard()->user();
+ $wallet = $user->wallet();
+
+ if (!$wallet->isController($user)) {
+ return $this->errorResponse(403);
+ }
+
+ // Validate the input
+ $v = Validator::make(
+ $request->all(),
+ [
+ 'description' => 'string|max:191'
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ DB::beginTransaction();
+
+ $room = Room::create([
+ 'description' => $request->input('description'),
+ ]);
+
+ $room->assignToWallet($wallet);
+
+ DB::commit();
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-create-success"),
+ ]);
+ }
+
+ /**
+ * Update a room.
+ *
+ * @param \Illuminate\Http\Request $request The API request.
+ * @param string $id Room identifier
+ *
+ * @return \Illuminate\Http\JsonResponse The response
+ */
+ public function update(Request $request, $id)
+ {
+ $room = $this->inputRoom($id, Share::ADMIN);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ // Validate the input
+ $v = Validator::make(
+ request()->all(),
+ [
+ 'description' => 'string|max:191'
+ ]
+ );
+
+ if ($v->fails()) {
+ return response()->json(['status' => 'error', 'errors' => $v->errors()], 422);
+ }
+
+ $room->description = request()->input('description');
+ $room->save();
+
+ // TODO: Update entitlements
+
+ return response()->json([
+ 'status' => 'success',
+ 'message' => \trans("app.room-update-success"),
+ ]);
+ }
+
+ /**
+ * Get the input room object, check permissions.
+ *
+ * @param int|string $id Room identifier (or name)
+ * @param ?int $rights Required access rights
+ *
+ * @return \App\Meet\Room|int File object or error code
+ */
+ protected function inputRoom($id, $rights = 0): int|Room
+ {
+ if (!is_numeric($id)) {
+ $room = Room::where('name', $id)->first();
+ } else {
+ $room = Room::find($id);
+ }
+
+ if (!$room) {
+ return 404;
+ }
+
+ $user = $this->guard()->user();
+
+ // Room owner (or another wallet controller)?
+ if ($room->wallet()->isController($user)) {
+ return $room;
+ }
+
+ if ($rights) {
+ $share = $room->shares()->where('user', $user->email)->first();
+
+ if ($share && $share->rights & $rights) {
+ return $room;
+ }
+ }
+
+ return 403;
+ }
+}
diff --git a/src/app/Http/Controllers/API/V4/SkusController.php b/src/app/Http/Controllers/API/V4/SkusController.php
--- a/src/app/Http/Controllers/API/V4/SkusController.php
+++ b/src/app/Http/Controllers/API/V4/SkusController.php
@@ -5,6 +5,7 @@
use App\Http\Controllers\ResourceController;
use App\Sku;
use Illuminate\Http\Request;
+use Illuminate\Support\Str;
class SkusController extends ResourceController
{
@@ -78,15 +79,15 @@
}
/**
- * Return SKUs available to the specified user/domain.
+ * Return SKUs available to the specified object.
*
- * @param object $object User or Domain object
+ * @param object $object User/Domain/etc object
*
* @return \Illuminate\Http\JsonResponse
*/
- protected static function objectSkus($object)
+ public static function objectSkus($object)
{
- $type = $object instanceof \App\Domain ? 'domain' : 'user';
+ $type = Str::kebab(\class_basename($object::class));
$response = [];
// Note: Order by title for consistent ordering in tests
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -165,6 +165,7 @@
// Check if the user is a controller of his wallet
$isController = $user->canDelete($user);
+ $isDegraded = $user->isDegraded();
$hasCustomDomain = $user->wallet()->entitlements()
->where('entitleable_type', Domain::class)
->count() > 0;
@@ -185,11 +186,12 @@
'enableDomains' => $isController && $hasCustomDomain,
// TODO: Make 'enableDistlists' working for wallet controllers that aren't account owners
'enableDistlists' => $isController && $hasCustomDomain && in_array('beta-distlists', $skus),
- 'enableFiles' => in_array('files', $skus),
+ 'enableFiles' => !$isDegraded && in_array('files', $skus),
// TODO: Make 'enableFolders' working for wallet controllers that aren't account owners
'enableFolders' => $isController && $hasCustomDomain && in_array('beta-shared-folders', $skus),
// TODO: Make 'enableResources' working for wallet controllers that aren't account owners
'enableResources' => $isController && $hasCustomDomain && in_array('beta-resources', $skus),
+ 'enableRooms' => !$isDegraded && in_array('meet', $skus),
'enableSettings' => $isController,
'enableUsers' => $isController,
'enableWallets' => $isController,
diff --git a/src/app/Http/Controllers/RelationController.php b/src/app/Http/Controllers/RelationController.php
--- a/src/app/Http/Controllers/RelationController.php
+++ b/src/app/Http/Controllers/RelationController.php
@@ -125,6 +125,8 @@
{
if ($full) {
$result = $object->toArray();
+
+ unset($result['tenant_id']);
} else {
$result = ['id' => $object->id];
diff --git a/src/app/Meet/Room.php b/src/app/Meet/Room.php
--- a/src/app/Meet/Room.php
+++ b/src/app/Meet/Room.php
@@ -2,21 +2,34 @@
namespace App\Meet;
+use App\Traits\BelongsToTenantTrait;
+use App\Traits\EntitleableTrait;
+use App\Traits\Meet\RoomConfigTrait;
use App\Traits\SettingsTrait;
+use App\Traits\ShareableTrait;
+use Dyrynda\Database\Support\NullableFields;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Cache;
/**
* The eloquent definition of a Room.
*
- * @property int $id Room identifier
- * @property string $name Room name
- * @property int $user_id Room owner
- * @property ?string $session_id Meet session identifier
+ * @property int $id Room identifier
+ * @property ?string $description Description
+ * @property string $name Room name
+ * @property int $tenant_id Tenant identifier
+ * @property ?string $session_id Meet session identifier
*/
class Room extends Model
{
+ use BelongsToTenantTrait;
+ use EntitleableTrait;
+ use RoomConfigTrait;
+ use NullableFields;
use SettingsTrait;
+ use ShareableTrait;
+ use SoftDeletes;
public const ROLE_SUBSCRIBER = 1 << 0;
public const ROLE_PUBLISHER = 1 << 1;
@@ -27,8 +40,18 @@
public const REQUEST_ACCEPTED = 'accepted';
public const REQUEST_DENIED = 'denied';
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'created_at' => 'datetime:Y-m-d H:i:s',
+ 'deleted_at' => 'datetime:Y-m-d H:i:s',
+ 'updated_at' => 'datetime:Y-m-d H:i:s',
+ ];
+
/** @var array<int, string> The attributes that are mass assignable */
- protected $fillable = ['user_id', 'name'];
+ protected $fillable = ['name', 'description'];
+
+ /** @var array<int, string> The attributes that can be not set */
+ protected $nullable = ['description'];
/** @var string Database table name */
protected $table = 'openvidu_rooms';
@@ -180,16 +203,6 @@
return $response->getStatusCode() == 200;
}
- /**
- * The room owner.
- *
- * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
- */
- public function owner()
- {
- return $this->belongsTo('\App\User', 'user_id', 'id');
- }
-
/**
* Accept the join request.
*
@@ -259,16 +272,6 @@
return Cache::put($this->session_id . '-' . $id, $request, now()->addHours(1));
}
- /**
- * Any (additional) properties of this room.
- *
- * @return \Illuminate\Database\Eloquent\Relations\HasMany
- */
- public function settings()
- {
- return $this->hasMany('App\Meet\RoomSetting', 'room_id');
- }
-
/**
* Send a signal to the Meet session participants (peers)
*
@@ -299,6 +302,28 @@
return $response->getStatusCode() == 200;
}
+ /**
+ * Returns a map of supported ACL labels.
+ *
+ * @return array Map of supported share rights/ACL labels
+ */
+ protected function supportedACL(): array
+ {
+ return [
+ 'full' => \App\Share::READ | \App\Share::WRITE | \App\Share::ADMIN,
+ ];
+ }
+
+ /**
+ * Returns room name (required by the EntitleableTrait).
+ *
+ * @return string|null Room name
+ */
+ public function toString(): ?string
+ {
+ return $this->name;
+ }
+
/**
* Log an error for a failed request to the meet server
*
diff --git a/src/app/Meet/RoomSetting.php b/src/app/Meet/RoomSetting.php
--- a/src/app/Meet/RoomSetting.php
+++ b/src/app/Meet/RoomSetting.php
@@ -14,10 +14,10 @@
*/
class RoomSetting extends Model
{
- protected $fillable = [
- 'room_id', 'key', 'value'
- ];
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = ['room_id', 'key', 'value'];
+ /** @var string Database table name */
protected $table = 'openvidu_room_settings';
/**
@@ -27,6 +27,6 @@
*/
public function room()
{
- return $this->belongsTo('\App\Meet\Room', 'room_id', 'id');
+ return $this->belongsTo(Room::class);
}
}
diff --git a/src/app/Observers/Meet/RoomObserver.php b/src/app/Observers/Meet/RoomObserver.php
new file mode 100644
--- /dev/null
+++ b/src/app/Observers/Meet/RoomObserver.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace App\Observers\Meet;
+
+use App\Meet\Room;
+
+class RoomObserver
+{
+ /**
+ * Handle the room "created" event.
+ *
+ * @param \App\Meet\Room $room The room
+ *
+ * @return void
+ */
+ public function creating(Room $room): void
+ {
+ if (empty($room->name)) {
+ // Generate a random and unique room name
+ while (true) {
+ $room->name = strtolower(\App\Utils::randStr(3, 3, '-'));
+ if (!Room::where('name', $room->name)->exists()) {
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -95,6 +95,8 @@
}
});
}
+
+ // TODO: Remove Share records for the user
}
/**
@@ -263,6 +265,10 @@
return;
}
+ if (!$entitlement->entitleable) {
+ return;
+ }
+
// Objects need to be deleted one by one to make sure observers can do the proper cleanup
if ($force) {
$entitlement->entitleable->forceDelete();
diff --git a/src/app/Providers/AppServiceProvider.php b/src/app/Providers/AppServiceProvider.php
--- a/src/app/Providers/AppServiceProvider.php
+++ b/src/app/Providers/AppServiceProvider.php
@@ -48,6 +48,7 @@
\App\Entitlement::observe(\App\Observers\EntitlementObserver::class);
\App\Group::observe(\App\Observers\GroupObserver::class);
\App\GroupSetting::observe(\App\Observers\GroupSettingObserver::class);
+ \App\Meet\Room::observe(\App\Observers\Meet\RoomObserver::class);
\App\PackageSku::observe(\App\Observers\PackageSkuObserver::class);
\App\PlanPackage::observe(\App\Observers\PlanPackageObserver::class);
\App\Resource::observe(\App\Observers\ResourceObserver::class);
diff --git a/src/app/Share.php b/src/app/Share.php
new file mode 100644
--- /dev/null
+++ b/src/app/Share.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a Share.
+ *
+ * @property int $rights Access rights
+ * @property string $user Permitted user (email)
+ * @property object $shareable The shared object
+ * @property int $shareable_id The shared object identifier
+ * @property string $shareable_type The shared object type (class name)
+ * @property string $id Share identifier
+ */
+class Share extends Model
+{
+ use UuidStrKeyTrait;
+
+ const READ = 1;
+ const WRITE = 2;
+ const ADMIN = 4;
+
+
+ /** @var array<int, string> The attributes that are mass assignable */
+ protected $fillable = [
+ 'shareable_id',
+ 'shareable_type',
+ 'rights',
+ 'user',
+ ];
+
+ /** @var array<string, string> The attributes that should be cast */
+ protected $casts = [
+ 'rights' => 'integer',
+ ];
+
+ /**
+ * Principally shareable object such as Room.
+ * Note that it may be trashed (soft-deleted).
+ *
+ * @return mixed
+ */
+ public function shareable()
+ {
+ return $this->morphTo()->withTrashed(); // @phpstan-ignore-line
+ }
+
+ /**
+ * Rights mutator. Make sure rights is integer.
+ */
+ public function setRightsAttribute($rights): void
+ {
+ $this->attributes['rights'] = $rights;
+ }
+}
diff --git a/src/app/Traits/EntitleableTrait.php b/src/app/Traits/EntitleableTrait.php
--- a/src/app/Traits/EntitleableTrait.php
+++ b/src/app/Traits/EntitleableTrait.php
@@ -94,7 +94,7 @@
// Note: it does not work with User/Domain model (yet)
$title = Str::kebab(\class_basename(self::class));
- $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ $sku = $this->skuByTitle($title);
$exists = $wallet->entitlements()->where('sku_id', $sku->id)->count();
Entitlement::create([
@@ -168,7 +168,7 @@
*/
public function hasSku(string $title): bool
{
- $sku = Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ $sku = $this->skuByTitle($title);
if (!$sku) {
return false;
@@ -234,6 +234,30 @@
}
}
+ /**
+ * Find the SKU object by title. Use current object's tenant context.
+ *
+ * @param string $title SKU title.
+ *
+ * @return ?\App\Sku A SKU object
+ */
+ protected function skuByTitle(string $title): ?Sku
+ {
+ return Sku::withObjectTenantContext($this)->where('title', $title)->first();
+ }
+
+ /**
+ * Returns entitleable object title (e.g. email or domain name).
+ *
+ * @return string|null An object title/name
+ */
+ public function toString(): ?string
+ {
+ // This method should be overloaded by the model class
+ // if the object has not email attribute
+ return $this->email;
+ }
+
/**
* Returns the wallet by which the object is controlled
*
diff --git a/src/app/Traits/Meet/RoomConfigTrait.php b/src/app/Traits/Meet/RoomConfigTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/Meet/RoomConfigTrait.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace App\Traits\Meet;
+
+trait RoomConfigTrait
+{
+ /**
+ * A helper to get the room configuration.
+ */
+ public function getConfig(): array
+ {
+ $settings = $this->getSettings(['password', 'locked', 'nomedia']);
+
+ $config = [
+ 'acl' => $this->getACL(),
+ 'locked' => $settings['locked'] === 'true',
+ 'nomedia' => $settings['nomedia'] === 'true',
+ 'password' => $settings['password'],
+ ];
+
+ return $config;
+ }
+
+ /**
+ * A helper to update room configuration.
+ *
+ * @param array $config An array of configuration options
+ *
+ * @return array A list of input validation error messages
+ */
+ public function setConfig(array $config): array
+ {
+ $errors = [];
+
+ foreach ($config as $key => $value) {
+ if ($key == 'password') {
+ if ($value === null || $value === '') {
+ $value = null;
+ } else {
+ // TODO: Do we have to validate the password in any way?
+ }
+ $this->setSetting($key, $value);
+ } elseif ($key == 'locked' || $key == 'nomedia') {
+ $this->setSetting($key, $value ? 'true' : null);
+ } elseif ($key == 'acl') {
+ $acl_errors = $this->validateACL($value);
+
+ if (empty($acl_errors)) {
+ $this->setACL($value);
+ } else {
+ $errors[$key] = $acl_errors;
+ }
+ } else {
+ $errors[$key] = \trans('validation.invalid-config-parameter');
+ }
+ }
+
+ return $errors;
+ }
+}
diff --git a/src/app/Traits/ShareableTrait.php b/src/app/Traits/ShareableTrait.php
new file mode 100644
--- /dev/null
+++ b/src/app/Traits/ShareableTrait.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace App\Traits;
+
+use App\Share;
+use Illuminate\Support\Facades\Validator;
+
+trait ShareableTrait
+{
+ /**
+ * Boot function from Laravel.
+ */
+ protected static function bootShareableTrait()
+ {
+ // Selete object's shares on object's delete
+ static::deleting(function ($model) {
+ $model->shares()->delete();
+ });
+ }
+
+ /**
+ * Shares for this object.
+ *
+ * @return \Illuminate\Database\Eloquent\Relations\HasMany
+ */
+ public function shares()
+ {
+ return $this->hasMany(Share::class, 'shareable_id', 'id')
+ ->where('shareable_type', self::class);
+ }
+
+ /**
+ * Validate ACL input
+ *
+ * @param mixed $input Common ACL input
+ *
+ * @return array List of validation errors
+ */
+ protected function validateACL(&$input): array
+ {
+ if (!is_array($input)) {
+ $input = (array) $input;
+ }
+
+ $users = [];
+ $errors = [];
+ $supported = $this->supportedACL();
+
+ foreach ($input as $i => $v) {
+ if (!is_string($v) || empty($v) || !substr_count($v, ',')) {
+ $errors[$i] = \trans('validation.acl-entry-invalid');
+ } else {
+ list($user, $acl) = explode(',', $v, 2);
+ $user = trim($user);
+ $acl = trim($acl);
+ $error = null;
+
+ if (
+ !isset($supported[$acl])
+ || ($error = $this->validateACLIdentifier($user))
+ || in_array($user, $users)
+ ) {
+ $errors[$i] = $error ?: \trans('validation.acl-entry-invalid');
+ }
+
+ $input[$i] = "$user, $acl";
+ $users[] = $user;
+ }
+ }
+
+ return $errors;
+ }
+
+ /**
+ * Validate an ACL identifier.
+ *
+ * @param string $identifier Email address
+ *
+ * @return ?string Error message on validation error
+ */
+ protected function validateACLIdentifier(string $identifier): ?string
+ {
+ $v = Validator::make(['email' => $identifier], ['email' => 'required|email']);
+
+ if ($v->fails()) {
+ return \trans('validation.emailinvalid');
+ }
+
+ $user = \App\User::where('email', \strtolower($identifier))->first();
+
+ if ($user) {
+ return null;
+ }
+
+ return \trans('validation.notalocaluser');
+ }
+
+ /**
+ * Build an ACL list from the object's shares
+ *
+ * @return array ACL list in a "common" format
+ */
+ protected function getACL(): array
+ {
+ $supported = $this->supportedACL();
+
+ return $this->shares()->get()
+ ->map(function ($share) use ($supported) {
+ $acl = array_search($share->rights, $supported) ?: 'none';
+ return "{$share->user}, {$acl}";
+ })
+ ->all();
+ }
+
+ /**
+ * Update the shares based on the ACL input.
+ *
+ * @param array $acl ACL list in a "common" format
+ */
+ protected function setACL(array $acl): void
+ {
+ $users = [];
+ $supported = $this->supportedACL();
+
+ foreach ($acl as $item) {
+ list($user, $right) = explode(',', $item, 2);
+ $users[\strtolower($user)] = $supported[trim($right)] ?? 0;
+ }
+
+ // Compare the input with existing shares
+ $this->shares()->get()->each(function ($share) use (&$users) {
+ if (isset($users[$share->user])) {
+ if ($share->rights != $users[$share->user]) {
+ $share->rights = $users[$share->user];
+ $share->save();
+ }
+ unset($users[$share->user]);
+ } else {
+ $share->delete();
+ }
+ });
+
+ foreach ($users as $user => $rights) {
+ $this->shares()->create([
+ 'user' => $user,
+ 'rights' => $rights,
+ 'shareable_type' => self::class,
+ ]);
+ }
+ }
+
+ /**
+ * Returns a map of supported ACL labels.
+ *
+ * @return array Map of supported share rights/ACL labels
+ */
+ protected function supportedACL(): array
+ {
+ return [
+ 'read-only' => Share::RIGHT_READ,
+ 'read-write' => Share::RIGHT_READ | Share::RIGHT_WRITE,
+ 'full' => Share::RIGHT_ADMIN,
+ ];
+ }
+}
diff --git a/src/app/Transaction.php b/src/app/Transaction.php
--- a/src/app/Transaction.php
+++ b/src/app/Transaction.php
@@ -158,7 +158,7 @@
$discount = $entitlement->wallet->getDiscountRate();
$result['entitlement_cost'] = $cost * $discount;
- $result['object'] = $entitlement->entitleableTitle();
+ $result['object'] = $entitlement->entitleable->toString();
$result['sku_title'] = $entitlement->sku->title;
} else {
$wallet = $this->wallet();
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -434,6 +434,19 @@
return $this->entitleables(Resource::class, $with_accounts);
}
+ /**
+ * Return rooms controlled by the current user.
+ *
+ * @param bool $with_accounts Include rooms assigned to wallets
+ * the current user controls but not owns.
+ *
+ * @return \Illuminate\Database\Eloquent\Builder Query builder
+ */
+ public function rooms($with_accounts = true)
+ {
+ return $this->entitleables(Meet\Room::class, $with_accounts);
+ }
+
/**
* Return shared folders controlled by the current user.
*
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -354,6 +354,18 @@
return (100 - $this->getDiscount()) / 100;
}
+ /**
+ * Check if the specified user is a controller to this wallet.
+ *
+ * @param \App\User $user The user object.
+ *
+ * @return bool True if the user is one of the wallet controllers (including user), False otherwise
+ */
+ public function isController(User $user): bool
+ {
+ return $user->id == $this->user_id || $this->controllers->contains($user);
+ }
+
/**
* A helper to display human-readable amount of money using
* the wallet currency and specified locale.
diff --git a/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php b/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
--- a/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
+++ b/src/database/migrations/2021_10_27_120000_extend_openvidu_rooms_session_id.php
@@ -32,7 +32,7 @@
Schema::table(
'openvidu_rooms',
function (Blueprint $table) {
- $table->string('session_id', 16)->change();
+ // $table->string('session_id', 16)->change();
}
);
}
diff --git a/src/database/migrations/2022_05_13_100000_shares_and_room_subscriptions.php b/src/database/migrations/2022_05_13_100000_shares_and_room_subscriptions.php
new file mode 100644
--- /dev/null
+++ b/src/database/migrations/2022_05_13_100000_shares_and_room_subscriptions.php
@@ -0,0 +1,126 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+ /**
+ * Run the migrations.
+ */
+ public function up(): void
+ {
+ Schema::create(
+ 'shares',
+ function (Blueprint $table) {
+ $table->string('id', 36)->primary();
+ $table->bigInteger('shareable_id');
+ $table->string('shareable_type');
+ $table->integer('rights')->default(0);
+ $table->string('user');
+ $table->timestamps();
+
+ $table->index('user');
+ $table->index(['shareable_id', 'shareable_type']);
+ }
+ );
+
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->bigInteger('tenant_id')->unsigned()->nullable();
+ $table->string('description')->nullable();
+ $table->softDeletes();
+
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('set null');
+ }
+ );
+
+ // Create the new SKU
+ if (!\App\Sku::where('title', 'room')->first()) {
+ $sku = \App\Sku::create([
+ 'title' => 'room',
+ 'name' => 'Conference room',
+ 'description' => 'Audio & Video Conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ]);
+
+ $counts = [];
+
+ // Create the entitlement for every room
+ foreach (\App\Meet\Room::get() as $room) {
+ $user = \App\User::find($room->user_id); // @phpstan-ignore-line
+ if (!$user) {
+ $room->forceDelete();
+ continue;
+ }
+
+ // Set tenant_id
+ if ($user->tenant_id) {
+ $room->tenant_id = $user->tenant_id;
+ $room->save();
+ }
+
+ $wallet = $user->wallet();
+ $counts[$wallet->id] = ($counts[$wallet->id] ?? 0) + 1;
+
+ \App\Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => $counts[$wallet->id] > $sku->units_free ? $sku->cost : 0,
+ 'fee' => $counts[$wallet->id] > $sku->units_free ? $sku->fee : 0,
+ 'entitleable_id' => $room->id,
+ 'entitleable_type' => \App\Meet\Room::class
+ ]);
+
+ // Set shares for room users that do not own them
+ if ($wallet->id != $user->wallets()->first()->id) {
+ $room->shares()->create([
+ 'user' => $user->email,
+ 'rights' => \App\Share::ADMIN,
+ ]);
+ }
+ }
+ }
+
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->dropForeign('openvidu_rooms_user_id_foreign');
+ $table->dropColumn('user_id');
+ }
+ );
+
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table(
+ 'openvidu_rooms',
+ function (Blueprint $table) {
+ $table->dropForeign('openvidu_rooms_tenant_id_foreign');
+ $table->dropColumn('tenant_id');
+ $table->dropColumn('description');
+ $table->dropSoftDeletes();
+
+ $table->bigInteger('user_id')->nullable();
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ }
+ );
+
+ // TODO: Set user_id back
+
+ \App\Entitlement::where('entitleable_type', \App\Meet\Room::class)->forceDelete();
+ \App\Sku::where('title', 'room')->delete();
+
+ Schema::dropIfExists('shares');
+ }
+};
diff --git a/src/database/seeds/local/MeetRoomSeeder.php b/src/database/seeds/local/MeetRoomSeeder.php
--- a/src/database/seeds/local/MeetRoomSeeder.php
+++ b/src/database/seeds/local/MeetRoomSeeder.php
@@ -2,7 +2,6 @@
namespace Database\Seeds\Local;
-use App\Meet\Room;
use Illuminate\Database\Seeder;
class MeetRoomSeeder extends Seeder
@@ -15,20 +14,29 @@
public function run()
{
$john = \App\User::where('email', 'john@kolab.org')->first();
- $jack = \App\User::where('email', 'jack@kolab.org')->first();
+ $wallet = $john->wallets()->first();
- \App\Meet\Room::create(
+ $rooms = [
[
- 'user_id' => $john->id,
- 'name' => 'john'
- ]
- );
-
- \App\Meet\Room::create(
+ 'name' => 'john',
+ 'description' => "John's room"
+ ],
[
- 'user_id' => $jack->id,
- 'name' => strtolower(\App\Utils::randStr(3, 3, '-'))
+ 'name' => 'jack',
+ 'description' => "Jack's room"
]
- );
+ ];
+
+ foreach ($rooms as $idx => $room) {
+ $room = \App\Meet\Room::create($room);
+ $room->assignToWallet($wallet);
+ $rooms[$idx] = $room;
+ }
+
+ $rooms[1]->shares()->create([
+ 'user' => 'jack@kolab.org',
+ 'rights' => \App\Share::ADMIN,
+ 'shareable_type' => \App\Meet\Room::class,
+ ]);
}
}
diff --git a/src/database/seeds/local/SkuSeeder.php b/src/database/seeds/local/SkuSeeder.php
--- a/src/database/seeds/local/SkuSeeder.php
+++ b/src/database/seeds/local/SkuSeeder.php
@@ -206,6 +206,21 @@
);
}
+ $sku = Sku::where(['title' => 'room', 'tenant_id' => \config('app.tenant_id')])->first();
+
+ if (!$sku) {
+ Sku::create([
+ 'title' => 'room',
+ 'name' => 'Conference room',
+ 'description' => 'Audio & Video Conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ]);
+ }
+
// Check existence because migration might have added this already
$sku = Sku::where(['title' => 'beta-distlists', 'tenant_id' => \config('app.tenant_id')])->first();
diff --git a/src/database/seeds/production/SkuSeeder.php b/src/database/seeds/production/SkuSeeder.php
--- a/src/database/seeds/production/SkuSeeder.php
+++ b/src/database/seeds/production/SkuSeeder.php
@@ -14,7 +14,7 @@
*/
public function run()
{
- Sku::create(
+ $skus = [
[
'title' => 'mailbox',
'name' => 'User Mailbox',
@@ -24,10 +24,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Mailbox',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain',
'name' => 'Hosted Domain',
@@ -36,10 +33,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Domain',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-registration',
'name' => 'Domain Registration',
@@ -48,10 +42,7 @@
'period' => 'yearly',
'handler_class' => 'App\Handlers\DomainRegistration',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-hosting',
'name' => 'External Domain',
@@ -61,10 +52,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainHosting',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'domain-relay',
'name' => 'Domain Relay',
@@ -73,10 +61,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\DomainRelay',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'storage',
'name' => 'Storage Quota',
@@ -86,10 +71,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Storage',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'groupware',
'name' => 'Groupware Features',
@@ -99,10 +81,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Groupware',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'resource',
'name' => 'Resource',
@@ -111,10 +90,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Resource',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'shared-folder',
'name' => 'Shared Folder',
@@ -123,10 +99,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\SharedFolder',
'active' => false,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => '2fa',
'name' => '2-Factor Authentication',
@@ -136,10 +109,7 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Auth2F',
'active' => true,
- ]
- );
-
- Sku::create(
+ ],
[
'title' => 'activesync',
'name' => 'Activesync',
@@ -149,60 +119,48 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
- ]
- );
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'beta')->first()) {
- Sku::create(
- [
- 'title' => 'beta',
- 'name' => 'Private Beta (invitation only)',
- 'description' => 'Access to the private beta program subscriptions',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta',
- 'active' => false,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'meet')->first()) {
- Sku::create(
- [
- 'title' => 'meet',
- 'name' => 'Voice & Video Conferencing (public beta)',
- 'description' => 'Video conferencing tool',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Meet',
- 'active' => true,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'group')->first()) {
- Sku::create(
- [
- 'title' => 'group',
- 'name' => 'Group',
- 'description' => 'Distribution list',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Group',
- 'active' => true,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'beta-distlists')->first()) {
- Sku::create([
+ ],
+ [
+ 'title' => 'beta',
+ 'name' => 'Private Beta (invitation only)',
+ 'description' => 'Access to the private beta program subscriptions',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta',
+ 'active' => false,
+ ],
+ [
+ 'title' => 'meet',
+ 'name' => 'Voice & Video Conferencing (public beta)',
+ 'description' => 'Video conferencing tool',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Meet',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'group',
+ 'name' => 'Group',
+ 'description' => 'Distribution list',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Group',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'room',
+ 'name' => 'Conference room',
+ 'description' => 'Audio & Video Conference room',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Room',
+ 'active' => true,
+ ],
+ [
'title' => 'beta-distlists',
'name' => 'Distribution lists',
'description' => 'Access to mail distribution lists',
@@ -211,12 +169,8 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\Distlists',
'active' => true,
- ]);
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'beta-resources')->first()) {
- Sku::create([
+ ],
+ [
'title' => 'beta-resources',
'name' => 'Calendaring resources',
'description' => 'Access to calendaring resources',
@@ -225,12 +179,8 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\Resources',
'active' => true,
- ]);
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'beta-shared-folders')->first()) {
- Sku::create([
+ ],
+ [
'title' => 'beta-shared-folders',
'name' => 'Shared folders',
'description' => 'Access to shared folders',
@@ -239,12 +189,8 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\SharedFolders',
'active' => true,
- ]);
- }
-
- // Check existence because migration might have added this already
- if (!Sku::where('title', 'files')->first()) {
- Sku::create([
+ ],
+ [
'title' => 'files',
'name' => 'File storage',
'description' => 'Access to file storage',
@@ -253,7 +199,14 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Files',
'active' => true,
- ]);
+ ],
+ ];
+
+ foreach ($skus as $sku) {
+ // Check existence because migration might have added this already
+ if (!Sku::where('title', $sku['title'])->first()) {
+ Sku::create($sku);
+ }
}
}
}
diff --git a/src/resources/js/user/routes.js b/src/resources/js/user/routes.js
--- a/src/resources/js/user/routes.js
+++ b/src/resources/js/user/routes.js
@@ -16,9 +16,10 @@
const DomainListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Domain/List')
const FileInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/Info')
const FileListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/File/List')
-const MeetComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Rooms')
const ResourceInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/Info')
const ResourceListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Resource/List')
+const RoomInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/Info')
+const RoomListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Room/List')
const SettingsComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Settings')
const SharedFolderInfoComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/Info')
const SharedFolderListComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/SharedFolder/List')
@@ -27,7 +28,7 @@
const UserProfileComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/Profile')
const UserProfileDeleteComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/User/ProfileDelete')
const WalletComponent = () => import(/* webpackChunkName: "../user/pages" */ '../../vue/Wallet')
-const RoomComponent = () => import(/* webpackChunkName: "../user/meet" */ '../../vue/Meet/Room.vue')
+const MeetComponent = () => import(/* webpackChunkName: "../user/meet" */ '../../vue/Meet/Room.vue')
const routes = [
{
@@ -88,6 +89,12 @@
name: 'logout',
component: LogoutComponent
},
+ {
+ name: 'meet',
+ path: '/meet/:room',
+ component: MeetComponent,
+ meta: { loading: true }
+ },
{
path: '/password-reset/:code?',
name: 'password-reset',
@@ -118,16 +125,16 @@
meta: { requiresAuth: true, perm: 'resources' }
},
{
- component: RoomComponent,
+ path: '/room/:room',
name: 'room',
- path: '/meet/:room',
- meta: { loading: true }
+ component: RoomInfoComponent,
+ meta: { requiresAuth: true, perm: 'rooms' }
},
{
path: '/rooms',
name: 'rooms',
- component: MeetComponent,
- meta: { requiresAuth: true }
+ component: RoomListComponent,
+ meta: { requiresAuth: true, perm: 'rooms' }
},
{
path: '/settings',
diff --git a/src/resources/lang/en/app.php b/src/resources/lang/en/app.php
--- a/src/resources/lang/en/app.php
+++ b/src/resources/lang/en/app.php
@@ -82,6 +82,12 @@
'resource-delete-success' => 'Resource deleted successfully.',
'resource-setconfig-success' => 'Resource settings updated successfully.',
+ 'room-update-success' => 'Room updated successfully.',
+ 'room-create-success' => 'Room created successfully.',
+ 'room-delete-success' => 'Room deleted successfully.',
+ 'room-setconfig-success' => 'Room configuration updated successfully.',
+ 'room-unsupported-option-error' => 'Invalid room configuration option.',
+
'shared-folder-update-success' => 'Shared folder updated successfully.',
'shared-folder-create-success' => 'Shared folder created successfully.',
'shared-folder-delete-success' => 'Shared folder deleted successfully.',
diff --git a/src/resources/lang/en/meet.php b/src/resources/lang/en/meet.php
--- a/src/resources/lang/en/meet.php
+++ b/src/resources/lang/en/meet.php
@@ -16,8 +16,6 @@
'connection-not-found' => 'The connection does not exist.',
'connection-dismiss-error' => 'Failed to dismiss the connection.',
'room-not-found' => 'The room does not exist.',
- 'room-setconfig-success' => 'Room configuration updated successfully.',
- 'room-unsupported-option-error' => 'Invalid room configuration option.',
'session-not-found' => 'The session does not exist.',
'session-create-error' => 'Failed to create the session.',
'session-join-error' => 'Failed to join the session.',
diff --git a/src/resources/lang/en/ui.php b/src/resources/lang/en/ui.php
--- a/src/resources/lang/en/ui.php
+++ b/src/resources/lang/en/ui.php
@@ -173,6 +173,7 @@
'shared-folder' => "Shared Folder",
'size' => "Size",
'status' => "Status",
+ 'subscriptions' => "Subscriptions",
'surname' => "Surname",
'type' => "Type",
'user' => "User",
@@ -214,9 +215,7 @@
],
'meet' => [
- 'title' => "Voice & Video Conferencing",
'welcome' => "Welcome to our beta program for Voice & Video Conferencing.",
- 'url' => "You have a room of your own at the URL below. This room is only open when you yourself are in attendance. Use this URL to invite people to join you.",
'notice' => "This is a work in progress and more features will be added over time. Current features include:",
'sharing' => "Screen Sharing",
'sharing-text' => "Share your screen for presentations or show-and-tell.",
@@ -354,6 +353,21 @@
'new' => "New resource",
],
+ 'room' => [
+ 'create' => "Create room",
+ 'delete' => "Delete room",
+ 'description-hint' => "This is an optional short description for the room, so you can find it more easily on the list.",
+ 'goto' => "Enter the room",
+ 'list-empty' => "There are no conference rooms in this account.",
+ 'list-title' => "Voice & video conferencing rooms",
+ 'moderators' => "Moderators",
+ 'moderators-text' => "You can share your room with other users. They will become the room moderators with all moderator powers and ability to open the room without your presence.",
+ 'new' => "New room",
+ 'new-hint' => "We'll generate a unique name for the room that will then allow you to access the room.",
+ 'title' => "Room: {name}",
+ 'url' => "You can access the room at the URL below. Use this URL to invite people to join you. This room is only open when you (or another room moderator) is in attendance.",
+ ],
+
'settings' => [
'password-policy' => "Password Policy",
'password-retention' => "Password Retention",
@@ -490,7 +504,6 @@
'search-pl' => "User ID, email or domain",
'skureq' => "{sku} requires {list}.",
'subscription' => "Subscription",
- 'subscriptions' => "Subscriptions",
'subscriptions-none' => "This user has no subscriptions.",
'users' => "Users",
],
diff --git a/src/resources/lang/fr/app.php b/src/resources/lang/fr/app.php
--- a/src/resources/lang/fr/app.php
+++ b/src/resources/lang/fr/app.php
@@ -74,6 +74,9 @@
'resource-delete-success' => "Ressource suprimmée avec succès.",
'resource-setconfig-success' => "Les paramètres des ressources ont été mis à jour avec succès.",
+ 'room-setconfig-success' => 'La configuration de la salle a été actualisée avec succès.',
+ 'room-unsupported-option-error' => 'Option de configuration de la salle invalide.',
+
'shared-folder-update-success' => "Dossier partagé mis à jour avec succès.",
'shared-folder-create-success' => "Dossier partagé créé avec succès.",
'shared-folder-delete-success' => "Dossier partagé supprimé avec succès.",
diff --git a/src/resources/lang/fr/meet.php b/src/resources/lang/fr/meet.php
--- a/src/resources/lang/fr/meet.php
+++ b/src/resources/lang/fr/meet.php
@@ -16,8 +16,6 @@
'connection-not-found' => 'La connexion n´existe pas.',
'connection-dismiss-error' => 'Échec du rejet de la connexion.',
'room-not-found' => 'La salle n´existe pas.',
- 'room-setconfig-success' => 'La configuration de la salle a été actualisée avec succès.',
- 'room-unsupported-option-error' => 'Option de configuration de la salle invalide.',
'session-not-found' => 'La session n\'existe pas.',
'session-create-error' => 'Échec de la création de la session.',
'session-join-error' => 'Échec de se joindre à la session.',
diff --git a/src/resources/lang/fr/ui.php b/src/resources/lang/fr/ui.php
--- a/src/resources/lang/fr/ui.php
+++ b/src/resources/lang/fr/ui.php
@@ -131,6 +131,7 @@
'phone' => "Téléphone",
'shared-folder' => "Dossier partagé",
'status' => "État",
+ 'subscriptions' => "Subscriptions",
'surname' => "Nom de famille",
'type' => "Type",
'user' => "Utilisateur",
@@ -176,9 +177,7 @@
],
'meet' => [
- 'title' => "Voix et vidéo-conférence",
'welcome' => "Bienvenue dans notre programme bêta pour les conférences vocales et vidéo.",
- 'url' => "Vous disposez d'une salle avec l'URL ci-dessous. Cette salle ouvre uniquement quand vous y êtes vous-même. Utilisez cette URL pour inviter des personnes à vous rejoindre.",
'notice' => "Il s'agit d'un travail en évolution et d'autres fonctions seront ajoutées au fil du temps. Les fonctions actuelles sont les suivantes:",
'sharing' => "Partage d'écran",
'sharing-text' => "Partagez votre écran pour des présentations ou des exposés.",
@@ -438,7 +437,6 @@
'search-pl' => "ID utilisateur, e-mail ou domamine",
'skureq' => "{sku} demande {list}.",
'subscription' => "Subscription",
- 'subscriptions' => "Subscriptions",
'subscriptions-none' => "Cet utilisateur n'a pas de subscriptions.",
'users' => "Utilisateurs",
],
diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue
--- a/src/resources/vue/Admin/User.vue
+++ b/src/resources/vue/Admin/User.vue
@@ -336,7 +336,7 @@
tabs: [
{ label: 'user.finances' },
{ label: 'user.aliases', count: 0 },
- { label: 'user.subscriptions', count: 0 },
+ { label: 'form.subscriptions', count: 0 },
{ label: 'user.domains', count: 0 },
{ label: 'user.users', count: 0 },
{ label: 'user.distlists', count: 0 },
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -28,11 +28,11 @@
<svg-icon icon="wallet"></svg-icon><span>{{ $t('dashboard.wallet') }}</span>
<span v-if="balance < 0" class="badge bg-danger">{{ $root.price(balance, currency) }}</span>
</router-link>
- <router-link v-if="$root.hasSKU('meet') && !$root.isDegraded()" class="card link-chat" :to="{ name: 'rooms' }">
+ <router-link v-if="status.enableRooms" class="card link-chat" :to="{ name: 'rooms' }">
<svg-icon icon="comments"></svg-icon><span>{{ $t('dashboard.chat') }}</span>
<span class="badge bg-primary">{{ $t('dashboard.beta') }}</span>
</router-link>
- <router-link v-if="status.enableFiles && !$root.isDegraded()" class="card link-files" :to="{ name: 'files' }">
+ <router-link v-if="status.enableFiles" class="card link-files" :to="{ name: 'files' }">
<svg-icon icon="folder-closed"></svg-icon><span>{{ $t('dashboard.files') }}</span>
<span class="badge bg-primary">{{ $t('dashboard.beta') }}</span>
</router-link>
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -30,7 +30,7 @@
<package-select class="col-sm-8 pt-sm-1" type="domain"></package-select>
</div>
<div v-if="domain.id" id="domain-skus" class="row">
- <label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
+ <label class="col-sm-4 col-form-label">{{ $t('form.subscriptions') }}</label>
<subscription-select v-if="domain.id" class="col-sm-8 pt-sm-1" type="domain" :object="domain" :readonly="true"></subscription-select>
</div>
<btn v-if="!domain.id" class="btn-primary mt-3" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
diff --git a/src/resources/vue/Meet/RoomOptions.vue b/src/resources/vue/Meet/RoomOptions.vue
--- a/src/resources/vue/Meet/RoomOptions.vue
+++ b/src/resources/vue/Meet/RoomOptions.vue
@@ -57,7 +57,7 @@
configSave(name, value, callback) {
const post = { [name]: value }
- axios.post('/api/v4/meet/rooms/' + this.room + '/config', post)
+ axios.post('/api/v4/rooms/' + this.room + '/config', post)
.then(response => {
this.$set(this.config, name, value)
if (callback) {
diff --git a/src/resources/vue/Room/Info.vue b/src/resources/vue/Room/Info.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Room/Info.vue
@@ -0,0 +1,162 @@
+<template>
+ <div class="container">
+ <div id="room-info" class="card">
+ <div class="card-body">
+ <div class="card-title" v-if="room.id">
+ {{ $t('room.title', { name: room.name }) }}
+ <btn v-if="room.canDelete" class="btn-outline-danger button-delete float-end" @click="roomDelete" icon="trash-can">{{ $t('room.delete') }}</btn>
+ </div>
+ <div class="card-title" v-else>{{ $t('room.new') }}</div>
+ <div class="card-text">
+ <div id="room-intro" class="pt-2">
+ <p v-if="room.id">{{ $t('room.url') }}</p>
+ <p v-if="room.id" class="text-center"><router-link :to="roomRoute">{{ href }}</router-link></p>
+ <p v-if="!room.id">{{ $t('room.new-hint') }}</p>
+ </div>
+ <tabs class="mt-3" :tabs="tabs"></tabs>
+ <div class="tab-content">
+ <div v-if="!room.id || room.isOwner" class="tab-pane show active" id="general" role="tabpanel" aria-labelledby="tab-general">
+ <form @submit.prevent="submit" class="card-body">
+ <div class="row mb-3">
+ <label for="description" class="col-sm-4 col-form-label">{{ $t('form.description') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" id="description" v-model="room.description">
+ <small class="form-text">{{ $t('room.description-hint') }}</small>
+ </div>
+ </div>
+ <div v-if="room.isOwner" id="room-skus" class="row mb-3">
+ <label class="col-sm-4 col-form-label">{{ $t('form.subscriptions') }}</label>
+ <subscription-select class="col-sm-8 pt-sm-1" ref="skus" :object="room" type="room"></subscription-select>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
+ </div>
+ <div v-if="room.canUpdate" :class="'tab-pane' + (!tabs.includes('form.general') ? ' show active' : '')" id="settings" role="tabpanel" aria-labelledby="tab-settings">
+ <form @submit.prevent="submitSettings" class="card-body">
+ <div class="row mb-3">
+ <label for="password" class="col-sm-4 col-form-label">{{ $t('form.password') }}</label>
+ <div class="col-sm-8">
+ <input type="text" class="form-control" v-model="room.config.password">
+ <span class="form-text">{{ $t('meet.password-text') }}</span>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="room-lock-input" class="col-sm-4 col-form-label">{{ $t('meet.lock') }}</label>
+ <div class="col-sm-8">
+ <input type="checkbox" id="room-lock-input" class="form-check-input d-block" v-model="room.config.locked">
+ <small class="form-text">{{ $t('meet.lock-text') }}</small>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="room-nomedia-input" class="col-sm-4 col-form-label">{{ $t('meet.nomedia') }}</label>
+ <div class="col-sm-8">
+ <input type="checkbox" id="room-nomedia-input" class="form-check-input d-block" v-model="room.config.nomedia">
+ <small class="form-text">{{ $t('meet.nomedia-text') }}</small>
+ </div>
+ </div>
+ <div class="row mb-3">
+ <label for="acl-input" class="col-sm-4 col-form-label">{{ $t('room.moderators') }}</label>
+ <div class="col-sm-8">
+ <acl-input id="acl" v-model="room.config.acl" :list="room.config.acl" :useronly="true" :types="['full']"></acl-input>
+ <small class="form-text">{{ $t('room.moderators-text') }}</small>
+ </div>
+ </div>
+ <btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
+ </form>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import AclInput from '../Widgets/AclInput'
+ import SubscriptionSelect from '../Widgets/SubscriptionSelect'
+
+ export default {
+ components: {
+ AclInput,
+ SubscriptionSelect
+ },
+ data() {
+ return {
+ href: '',
+ room: { config: { acl: [] } },
+ roomRoute: ''
+ }
+ },
+ computed: {
+ tabs() {
+ let tabs = []
+
+ if (!this.room.id || this.room.isOwner) {
+ tabs.push('form.general')
+ }
+
+ if (this.room.canUpdate) {
+ tabs.push('form.settings')
+ }
+
+ return tabs
+ },
+ },
+ created() {
+ const room_id = this.$route.params.room
+
+ if (room_id != 'new') {
+ axios.get('/api/v4/rooms/' + room_id, { loader: true })
+ .then(response => {
+ this.room = response.data
+ this.roomRoute = '/meet/' + encodeURI(this.room.name)
+ this.href = window.config['app.url'] + this.roomRoute
+ })
+ .catch(this.$root.errorHandler)
+ }
+ },
+ mounted() {
+ $('#description').focus()
+ },
+ methods: {
+ roomDelete() {
+ axios.delete('/api/v4/rooms/' + this.room.id)
+ .then(response => {
+ if (response.data.status == 'success') {
+ this.$toast.success(response.data.message)
+ this.$router.push('/rooms')
+ }
+ })
+ },
+ submit() {
+ this.$root.clearFormValidation($('#general form'))
+
+ let method = 'post'
+ let location = '/api/v4/rooms'
+ let post = this.$root.pick(this.room, ['description'])
+
+ if (this.room.id) {
+ method = 'put'
+ location += '/' + this.room.id
+ post.skus = this.$refs.skus.getSkus()
+ }
+
+ axios[method](location, post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ this.$router.push('/rooms')
+ })
+ },
+ submitSettings() {
+ this.$root.clearFormValidation($('#settings form'))
+
+ const post = this.$root.pick(this.room.config, [ 'password', 'acl', 'locked', 'nomedia' ])
+
+ axios.post('/api/v4/rooms/' + this.room.id + '/config', post)
+ .then(response => {
+ this.$toast.success(response.data.message)
+ })
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Room/List.vue b/src/resources/vue/Room/List.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Room/List.vue
@@ -0,0 +1,71 @@
+<template>
+ <div class="container">
+ <div id="rooms-list" class="card">
+ <div class="card-body">
+ <div class="card-title">
+ {{ $t('room.list-title') }}
+ <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small>
+ <btn-router v-if="!$root.isDegraded() && $root.hasPermission('settings')" to="room/new" class="btn-success float-end" icon="comments">
+ {{ $t('room.create') }}
+ </btn-router>
+ </div>
+ <div class="card-text">
+ <list-table :list="rooms" :setup="setup">
+ <template #buttons="{ item }">
+ <btn class="button-link p-0 ms-1" @click="goto(item)" icon="arrow-up-right-from-square" :title="$t('room.goto')"></btn>
+ </template>
+ </list-table>
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+ import { ListTable } from '../Widgets/ListTools'
+ import { library } from '@fortawesome/fontawesome-svg-core'
+
+ library.add(
+ require('@fortawesome/free-solid-svg-icons/faArrowUpRightFromSquare').definition,
+ require('@fortawesome/free-solid-svg-icons/faComments').definition
+ )
+
+ export default {
+ components: {
+ ListTable
+ },
+ data() {
+ return {
+ rooms: [],
+ setup: {
+ buttons: true,
+ model: 'room',
+ columns: [
+ {
+ prop: 'name',
+ icon: 'comments',
+ link: true
+ },
+ {
+ prop: 'description',
+ link: true
+ }
+ ]
+ }
+ }
+ },
+ mounted() {
+ axios.get('/api/v4/rooms', { loader: true })
+ .then(response => {
+ this.rooms = response.data.list
+ })
+ .catch(this.$root.errorHandler)
+ },
+ methods: {
+ goto(room) {
+ const location = window.config['app.url'] + '/meet/' + encodeURI(room.name)
+ window.open(location, '_blank')
+ }
+ }
+ }
+</script>
diff --git a/src/resources/vue/Rooms.vue b/src/resources/vue/Rooms.vue
deleted file mode 100644
--- a/src/resources/vue/Rooms.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<template>
- <div class="container" dusk="rooms-component">
- <div id="meet-rooms" class="card">
- <div class="card-body">
- <div class="card-title">{{ $t('meet.title') }} <small><sup class="badge bg-primary">{{ $t('dashboard.beta') }}</sup></small></div>
- <div class="card-text">
- <p>{{ $t('meet.welcome') }}</p>
- <p>{{ $t('meet.url') }}</p>
- <p><router-link v-if="href" :to="roomRoute">{{ href }}</router-link></p>
- <p>{{ $t('meet.notice') }}</p>
- <dl>
- <dt>{{ $t('meet.sharing') }}</dt>
- <dd>{{ $t('meet.sharing-text') }}</dd>
- <dt>{{ $t('meet.security') }}</dt>
- <dd>{{ $t('meet.security-text') }}</dd>
- <dt>{{ $t('meet.qa-title') }}</dt>
- <dd>{{ $t('meet.qa-text') }}</dd>
- <dt>{{ $t('meet.moderation') }}</dt>
- <dd>{{ $t('meet.moderation-text') }}</dd>
- <dt>{{ $t('meet.eject') }}</dt>
- <dd>{{ $t('meet.eject-text') }}</dd>
- <dt>{{ $t('meet.silent') }}</dt>
- <dd>{{ $t('meet.silent-text') }}</dd>
- <dt>{{ $t('meet.interpreters') }}</dt>
- <dd>{{ $t('meet.interpreters-text') }}</dd>
- </dl>
- <p>{{ $t('meet.beta-notice') }}</p>
- </div>
- </div>
- </div>
- </div>
-</template>
-
-<script>
- export default {
- data() {
- return {
- rooms: [],
- href: '',
- roomRoute: ''
- }
- },
- mounted() {
- if (!this.$root.hasSKU('meet') || this.$root.isDegraded()) {
- this.$root.errorPage(403)
- return
- }
-
- axios.get('/api/v4/meet/rooms', { loader: true })
- .then(response => {
- this.rooms = response.data.list
- if (response.data.count) {
- this.roomRoute = '/meet/' + encodeURI(this.rooms[0].name)
- this.href = window.config['app.url'] + this.roomRoute
- }
- })
- .catch(this.$root.errorHandler)
- }
- }
-</script>
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -76,8 +76,8 @@
<package-select class="col-sm-8 pt-sm-1"></package-select>
</div>
<div v-if="user_id !== 'new'" id="user-skus" class="row mb-3">
- <label class="col-sm-4 col-form-label">{{ $t('user.subscriptions') }}</label>
- <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user"></subscription-select>
+ <label class="col-sm-4 col-form-label">{{ $t('form.subscriptions') }}</label>
+ <subscription-select v-if="user.id" class="col-sm-8 pt-sm-1" :object="user" ref="skus"></subscription-select>
</div>
<btn class="btn-primary" type="submit" icon="check">{{ $t('btn.submit') }}</btn>
</form>
@@ -231,15 +231,7 @@
if (this.user_id !== 'new') {
method = 'put'
location += '/' + this.user_id
-
- let skus = {}
- $('#user-skus input[type=checkbox]:checked').each((idx, input) => {
- let id = $(input).val()
- let range = $(input).parents('tr').first().find('input[type=range]').val()
-
- skus[id] = range || 1
- })
- post.skus = skus
+ post.skus = this.$refs.skus.getSkus()
} else {
post.package = $('#user-packages input:checked').val()
}
diff --git a/src/resources/vue/Widgets/AclInput.vue b/src/resources/vue/Widgets/AclInput.vue
--- a/src/resources/vue/Widgets/AclInput.vue
+++ b/src/resources/vue/Widgets/AclInput.vue
@@ -6,7 +6,7 @@
<option value="anyone">{{ $t('form.anyone') }}</option>
</select>
<input :id="id + '-input'" type="text" class="form-control main-input" :placeholder="$t('form.email')" @keydown="keyDown">
- <select class="form-select acl" v-model="perm">
+ <select v-if="types.length > 1" class="form-select acl" v-model="perm">
<option v-for="t in types" :key="t" :value="t">{{ $t('form.acl-' + t) }}</option>
</select>
<a href="#" class="btn btn-outline-secondary" @click.prevent="addItem">
@@ -15,7 +15,7 @@
</div>
<div class="input-group" v-for="(item, index) in list" :key="index">
<input type="text" class="form-control" :value="aclIdent(item)" :readonly="aclIdent(item) == 'anyone'" :placeholder="$t('form.email')">
- <select class="form-select acl">
+ <select v-if="types.length > 1" class="form-select acl">
<option v-for="t in types" :key="t" :value="t" :selected="aclPerm(item) == t">{{ $t('form.acl-' + t) }}</option>
</select>
<a href="#" class="btn btn-outline-secondary" @click.prevent="deleteItem(index)">
@@ -26,17 +26,19 @@
</template>
<script>
+ const DEFAULT_TYPES = [ 'read-only', 'read-write', 'full' ]
+
export default {
props: {
list: { type: Array, default: () => [] },
id: { type: String, default: '' },
+ types: { type: Array, default: () => DEFAULT_TYPES },
useronly: { type: Boolean, default: false }
},
data() {
return {
mod: 'user',
perm: 'read-only',
- types: [ 'read-only', 'read-write', 'full' ]
}
},
mounted() {
@@ -68,7 +70,9 @@
value = 'anyone'
}
- this.$set(this.list, this.list.length, value + ', ' + this.perm)
+ const perm = this.types.length > 1 ? this.perm : this.types[0]
+
+ this.$set(this.list, this.list.length, value + ', ' + perm)
this.input.classList.remove('is-invalid')
this.input.value = ''
@@ -109,7 +113,7 @@
updateList() {
// Update this.list to the current state of the html elements
$(this.$el).children('.input-group:not(:first-child)').each((index, elem) => {
- const perm = $(elem).find('select.acl').val()
+ const perm = this.types.length > 1 ? $(elem).find('select.acl').val() : this.types[0]
const value = $(elem).find('input').val()
this.$set(this.list, index, value + ', ' + perm)
})
diff --git a/src/resources/vue/Widgets/SubscriptionSelect.vue b/src/resources/vue/Widgets/SubscriptionSelect.vue
--- a/src/resources/vue/Widgets/SubscriptionSelect.vue
+++ b/src/resources/vue/Widgets/SubscriptionSelect.vue
@@ -196,6 +196,18 @@
// Update the price
record.find('.price').text(this.$root.priceLabel(cost, this.discount, this.currency))
+ },
+ getSkus() {
+ let skus = {}
+
+ $(this.$el).find('input[type=checkbox]:checked').each((idx, input) => {
+ let id = $(input).val()
+ let range = $(input).parents('tr').first().find('input[type=range]').val()
+
+ skus[id] = range || 1
+ })
+
+ return skus
}
}
}
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -98,8 +98,10 @@
Route::apiResource('packages', API\V4\PackagesController::class);
- Route::get('meet/rooms', [API\V4\MeetController::class, 'index']);
- Route::post('meet/rooms/{id}/config', [API\V4\MeetController::class, 'setRoomConfig']);
+ Route::apiResource('rooms', API\V4\RoomsController::class);
+ Route::post('rooms/{id}/config', [API\V4\RoomsController::class, 'setConfig']);
+ Route::get('rooms/{id}/skus', [API\V4\RoomsController::class, 'skus']);
+
Route::post('meet/rooms/{id}', [API\V4\MeetController::class, 'joinRoom'])
->withoutMiddleware(['auth:api']);
diff --git a/src/tests/Browser/Meet/RoomSetupTest.php b/src/tests/Browser/Meet/RoomSetupTest.php
--- a/src/tests/Browser/Meet/RoomSetupTest.php
+++ b/src/tests/Browser/Meet/RoomSetupTest.php
@@ -264,9 +264,8 @@
$guest->with(new Dialog('#leave-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Room closed')
->assertSeeIn('@body', "The session has been closed by the room owner.")
- ->assertMissing('@button-cancel')
- ->assertSeeIn('@button-action', 'Close')
- ->click('@button-action');
+ ->assertSeeIn('@button-cancel', 'Close')
+ ->click('@button-cancel');
})
->assertMissing('#leave-dialog')
->waitForLocation('/login');
@@ -461,7 +460,7 @@
$guest1
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Media setup')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->waitFor('@session .meet-video.self')
->assertElementsCount('@session div.meet-video', 2)
@@ -495,7 +494,7 @@
->waitUntilMissing('@session .meet-subscriber.self')
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->assertSeeIn('@title', 'Media setup')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->waitFor('@session div.meet-video.self')
->assertElementsCount('@session div.meet-video', 2)
@@ -546,9 +545,8 @@
->assertVisible('form > div:nth-child(3) svg')
->assertAttribute('form > div:nth-child(3) .input-group-text', 'title', 'Camera')
->assertVisible('form > div:nth-child(3) select')
- ->assertMissing('@button-cancel')
- ->assertSeeIn('@button-action', 'Close')
- ->click('@button-action');
+ ->assertSeeIn('@button-cancel', 'Close')
+ ->click('@button-cancel');
})
->assertMissing('#media-setup-dialog')
// Test mute audio and video
@@ -556,7 +554,7 @@
->with(new Dialog('#media-setup-dialog'), function (Browser $browser) {
$browser->select('form > div:nth-child(2) select', '')
->select('form > div:nth-child(3) select', '')
- ->click('@button-action');
+ ->click('@button-cancel');
})
->assertMissing('#media-setup-dialog')
->assertVisible('@session .meet-video .status .status-audio')
diff --git a/src/tests/Browser/Meet/RoomsTest.php b/src/tests/Browser/Meet/RoomsTest.php
--- a/src/tests/Browser/Meet/RoomsTest.php
+++ b/src/tests/Browser/Meet/RoomsTest.php
@@ -2,12 +2,14 @@
namespace Tests\Browser\Meet;
-use App\Sku;
+use App\Meet\Room;
use Tests\Browser;
use Tests\Browser\Components\Toast;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
use Tests\Browser\Pages\Meet\Room as RoomPage;
+use Tests\Browser\Pages\Meet\RoomInfo;
+use Tests\Browser\Pages\Meet\RoomList;
use Tests\Browser\Pages\UserInfo;
use Tests\TestCaseDusk;
@@ -19,7 +21,9 @@
public function setUp(): void
{
parent::setUp();
+
$this->clearMeetEntitlements();
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
}
/**
@@ -28,13 +32,20 @@
public function tearDown(): void
{
$this->clearMeetEntitlements();
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+ $room = Room::where('name', 'jack')->first();
+ $room->setConfig([
+ 'password' => '',
+ 'locked' => false,
+ 'nomedia' => false,
+ 'acl' => ['jack@kolab.org, full']
+ ]);
+
parent::tearDown();
}
/**
* Test rooms page (unauthenticated and unauthorized)
- *
- * @group meet
*/
public function testRoomsUnauth(): void
{
@@ -51,14 +62,13 @@
}
/**
- * Test rooms page
+ * Test rooms list page
*
* @group meet
*/
public function testRooms(): void
{
$this->browse(function (Browser $browser) {
- $href = \config('app.url') . '/meet/john';
$john = $this->getTestUser('john@kolab.org');
// User has no 'meet' entitlement yet
@@ -71,10 +81,9 @@
// Goto user subscriptions, and enable 'meet' subscription
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
- ->whenAvailable('@skus', function ($browser) {
- $browser->click('#sku-input-meet');
- })
- ->scrollTo('#general button[type=submit]')->pause(200)
+ ->waitFor('@skus')
+ ->scrollTo('#sku-input-meet')->pause(500)
+ ->click('#sku-input-meet')
->click('#general button[type=submit]')
->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
->click('.navbar-brand')
@@ -87,13 +96,29 @@
// Test Video chat page
$browser->click('@links a.link-chat')
- ->waitFor('#meet-rooms')
- ->waitFor('.card-text a')
- ->assertSeeIn('.card-title', 'Voice & Video Conferencing')
- ->assertSeeIn('.card-text a', $href)
- ->assertAttribute('.card-text a', 'href', '/meet/john')
- ->click('.card-text a')
- ->on(new RoomPage('john'))
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2)
+ ->assertElementsCount('thead th', 3)
+ ->with('tbody tr:nth-child(1)', function ($browser) {
+ $browser->assertSeeIn('td:nth-child(1) a', 'jack')
+ ->assertSeeIn('td:nth-child(2) a', "Jack's room")
+ ->assertVisible('td.buttons button.button-link')
+ ->assertAttribute('td.buttons button.button-link', 'title', 'Enter the room');
+ })
+ ->with('tbody tr:nth-child(2)', function ($browser) {
+ $browser->assertSeeIn('td:nth-child(1) a', 'john')
+ ->assertSeeIn('td:nth-child(2) a', "John's room")
+ ->assertVisible('td.buttons button.button-link')
+ ->assertAttribute('td.buttons button.button-link', 'title', 'Enter the room');
+ })
+ ->click('tbody tr:nth-child(2) button.button-link');
+ });
+
+ $newWindow = collect($browser->driver->getWindowHandles())->last();
+ $browser->driver->switchTo()->window($newWindow);
+
+ $browser->on(new RoomPage('john'))
// check that entering the room skips the logon form
->assertMissing('@toolbar')
->assertMissing('@menu')
@@ -108,4 +133,214 @@
->assertMissing('@setup-form');
});
}
+
+ /**
+ * Test rooms create and edit and delete
+ */
+ public function testRoomCreateAndEditAndDelete(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $this->assignMeetEntitlement($john);
+
+ $this->browse(function (Browser $browser) {
+ // Test room creation
+ $browser->visit(new RoomList())
+ ->assertSeeIn('button.room-new', 'Create room')
+ ->click('button.room-new')
+ ->on(new RoomInfo())
+ ->assertVisible('@intro p')
+ ->assertElementsCount('@nav li', 1)
+ ->assertSeeIn('@nav li a', 'General')
+ ->with('@general form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Description')
+ ->assertFocused('.row:nth-child(1) input')
+ ->type('.row:nth-child(1) input', 'test123');
+ })
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room created successfully.")
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 3);
+ });
+
+ $room = Room::where('description', 'test123')->first();
+
+ // Test room editing
+ $browser->click("a[href=\"/room/{$room->id}\"]")
+ ->on(new RoomInfo())
+ ->assertSeeIn('.card-title', "Room: {$room->name}")
+ ->assertVisible('@intro p')
+ ->assertVisible("@intro a[href=\"/meet/{$room->name}\"]")
+ ->assertElementsCount('@nav li', 2)
+ ->assertSeeIn('@nav li:first-child a', 'General')
+ ->with('@general form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Description')
+ ->assertFocused('.row:nth-child(1) input')
+ ->type('.row:nth-child(1) input', 'test321')
+ ->assertSeeIn('.row:nth-child(2) label', 'Subscriptions')
+ ->with('@skus', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->assertSeeIn('tbody tr:nth-child(1) td.name', 'Conference room')
+ ->assertSeeIn('tbody tr:nth-child(1) td.price', '0,00 CHF/month')
+ ->assertChecked('tbody tr:nth-child(1) td.selection input')
+ ->assertDisabled('tbody tr:nth-child(1) td.selection input')
+ ->assertTip(
+ 'tbody tr:nth-child(1) td.buttons button',
+ 'Audio & Video Conference room'
+ );
+ });
+ })
+ ->click('@general button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room updated successfully.")
+ ->on(new RoomList());
+
+ $room->refresh();
+
+ $this->assertSame('test321', $room->description);
+
+ // Test room settings editing
+ $browser->click("a[href=\"/room/{$room->id}\"]")
+ ->on(new RoomInfo())
+ ->assertSeeIn('@nav li:last-child a', 'Settings')
+ ->click('@nav li:last-child a')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Password')
+ ->assertValue('.row:nth-child(1) input', '')
+ ->assertVisible('.row:nth-child(1) .form-text')
+ ->assertSeeIn('.row:nth-child(2) label', 'Locked room')
+ ->assertNotChecked('.row:nth-child(2) input')
+ ->assertVisible('.row:nth-child(2) .form-text')
+ ->assertSeeIn('.row:nth-child(3) label', 'Subscribers only')
+ ->assertNotChecked('.row:nth-child(3) input')
+ ->assertVisible('.row:nth-child(3) .form-text')
+ ->assertSeeIn('.row:nth-child(4) label', 'Moderators')
+ ->assertVisible('.row:nth-child(4) .form-text')
+ ->type('#acl .input-group:first-child input', 'jack')
+ ->click('#acl a.btn');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_ERROR, "Form validation error")
+ ->assertSeeIn('#acl + .invalid-feedback', "The specified email address is invalid.")
+ ->with('@settings form', function ($browser) {
+ $browser->type('.row:nth-child(1) input', 'pass')
+ ->click('.row:nth-child(2) input')
+ ->click('.row:nth-child(3) input')
+ ->click('#acl .input-group:last-child a.btn')
+ ->type('#acl .input-group:first-child input', 'jack@kolab.org')
+ ->click('#acl a.btn');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.");
+
+ $config = $room->getConfig();
+
+ $this->assertSame('pass', $config['password']);
+ $this->assertSame(true, $config['locked']);
+ $this->assertSame(true, $config['nomedia']);
+ $this->assertSame(['jack@kolab.org, full'], $config['acl']);
+
+ // Test room deleting
+ $browser->assertSeeIn('button.button-delete', 'Delete room')
+ ->click('button.button-delete')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room deleted successfully.")
+ ->on(new RoomList())
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 2);
+ });
+ });
+ }
+
+ /**
+ * Test acting as a non-controller user
+ *
+ * @group meet
+ */
+ public function testNonControllerRooms(): void
+ {
+ $jack = $this->getTestUser('jack@kolab.org');
+ $this->assignMeetEntitlement($jack);
+ $john = $this->getTestUser('john@kolab.org');
+ $this->assignMeetEntitlement($john);
+
+ $room = Room::where('name', 'jack')->first();
+ $room->session_id = null;
+ $room->save();
+ $room->setConfig([
+ 'password' => 'pass',
+ 'locked' => true,
+ 'nomedia' => true,
+ 'acl' => ['jack@kolab.org, full']
+ ]);
+
+ $this->browse(function (Browser $browser) use ($room) {
+ $browser->visit(new Home())
+ ->submitLogon('jack@kolab.org', 'simple123', true)
+ ->on(new Dashboard())
+ ->click('@links a.link-chat')
+ ->on(new RoomList())
+ ->assertMissing('button.room-new')
+ ->assertMissing('button.button-delete')
+ ->whenAvailable('@table', function ($browser) {
+ $browser->assertElementsCount('tbody tr', 1)
+ ->with('tbody tr:nth-child(1)', function ($browser) {
+ $browser->assertSeeIn('td:nth-child(1) a', 'jack')
+ ->assertSeeIn('td:nth-child(2) a', "Jack's room")
+ ->assertVisible('td.buttons button.button-link')
+ ->assertAttribute('td.buttons button.button-link', 'title', 'Enter the room');
+ })
+ ->click('tbody tr:nth-child(1)');
+ })
+ ->on(new RoomInfo())
+ ->assertSeeIn('.card-title', "Room: {$room->name}")
+ ->assertVisible('@intro p')
+ ->assertVisible("@intro a[href=\"/meet/{$room->name}\"]")
+ ->assertElementsCount('@nav li', 1)
+ // Test room settings
+ ->assertSeeIn('@nav li:last-child a', 'Settings')
+ ->with('@settings form', function ($browser) {
+ $browser->assertSeeIn('.row:nth-child(1) label', 'Password')
+ ->assertValue('.row:nth-child(1) input', 'pass')
+ ->assertVisible('.row:nth-child(1) .form-text')
+ ->assertSeeIn('.row:nth-child(2) label', 'Locked room')
+ ->assertChecked('.row:nth-child(2) input')
+ ->assertVisible('.row:nth-child(2) .form-text')
+ ->assertSeeIn('.row:nth-child(3) label', 'Subscribers only')
+ ->assertChecked('.row:nth-child(3) input')
+ ->assertVisible('.row:nth-child(3) .form-text')
+ ->assertSeeIn('.row:nth-child(4) label', 'Moderators')
+ ->assertValue('#acl .input-group:last-child input', 'jack@kolab.org')
+ ->assertVisible('.row:nth-child(4) .form-text')
+ ->type('#acl .input-group:first-child input', 'joe@kolab.org')
+ ->click('#acl a.btn')
+ ->type('.row:nth-child(1) input', 'pass123')
+ ->click('.row:nth-child(2) input')
+ ->click('.row:nth-child(3) input');
+ })
+ ->click('@settings button[type=submit]')
+ ->assertToast(Toast::TYPE_SUCCESS, "Room configuration updated successfully.");
+
+ $config = $room->getConfig();
+
+ $this->assertSame('pass123', $config['password']);
+ $this->assertSame(false, $config['locked']);
+ $this->assertSame(false, $config['nomedia']);
+ $this->assertSame(['joe@kolab.org, full', 'jack@kolab.org, full'], $config['acl']);
+
+ $browser->click("@intro a[href=\"/meet/{$room->name}\"]")
+ ->on(new RoomPage('jack'))
+ // check that entering the room skips the logon form
+ ->assertMissing('@toolbar')
+ ->assertMissing('@menu')
+ ->assertMissing('@session')
+ ->assertMissing('@chat')
+ ->assertMissing('@login-form')
+ ->assertVisible('@setup-form')
+ ->assertSeeIn('@setup-status-message', "The room is closed. It will be open for others after you join.")
+ ->assertSeeIn('@setup-button', "JOIN")
+ ->click('@setup-button')
+ ->waitFor('@session')
+ ->assertMissing('@setup-form')
+ ->waitFor('a.meet-nickname svg.moderator');
+ });
+ }
}
diff --git a/src/tests/Browser/Pages/Meet/RoomInfo.php b/src/tests/Browser/Pages/Meet/RoomInfo.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Meet/RoomInfo.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Tests\Browser\Pages\Meet;
+
+use Laravel\Dusk\Page;
+
+class RoomInfo extends Page
+{
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '';
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->waitUntilMissing('@app .app-loader');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@intro' => '#room-intro',
+ '@general' => '#general',
+ '@nav' => 'ul.nav-tabs',
+ '@settings' => '#settings',
+ '@skus' => '#room-skus table',
+ ];
+ }
+}
diff --git a/src/tests/Browser/Pages/Meet/RoomList.php b/src/tests/Browser/Pages/Meet/RoomList.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Pages/Meet/RoomList.php
@@ -0,0 +1,46 @@
+<?php
+
+namespace Tests\Browser\Pages\Meet;
+
+use Laravel\Dusk\Page;
+
+class RoomList extends Page
+{
+ /**
+ * Get the URL for the page.
+ *
+ * @return string
+ */
+ public function url(): string
+ {
+ return '/rooms';
+ }
+
+ /**
+ * Assert that the browser is on the page.
+ *
+ * @param \Laravel\Dusk\Browser $browser The browser object
+ *
+ * @return void
+ */
+ public function assert($browser)
+ {
+ $browser->assertPathIs($this->url())
+ ->waitUntilMissing('@app .app-loader')
+ ->assertSeeIn('@list .card-title', 'Voice & video conferencing rooms');
+ }
+
+ /**
+ * Get the element shortcuts for the page.
+ *
+ * @return array
+ */
+ public function elements(): array
+ {
+ return [
+ '@app' => '#app',
+ '@list' => '#rooms-list',
+ '@table' => '#rooms-list table',
+ ];
+ }
+}
diff --git a/src/tests/Feature/Controller/Admin/SkusTest.php b/src/tests/Feature/Controller/Admin/SkusTest.php
--- a/src/tests/Feature/Controller/Admin/SkusTest.php
+++ b/src/tests/Feature/Controller/Admin/SkusTest.php
@@ -82,7 +82,7 @@
$json = $response->json();
- $this->assertCount(14, $json);
+ $this->assertCount(15, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
diff --git a/src/tests/Feature/Controller/MeetTest.php b/src/tests/Feature/Controller/MeetTest.php
--- a/src/tests/Feature/Controller/MeetTest.php
+++ b/src/tests/Feature/Controller/MeetTest.php
@@ -20,6 +20,9 @@
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
$this->clearMeetEntitlements();
@@ -29,42 +32,6 @@
parent::tearDown();
}
- /**
- * Test listing user rooms
- *
- * @group meet
- */
- public function testIndex(): void
- {
- $john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
- Room::where('user_id', $jack->id)->delete();
-
- // Unauth access not allowed
- $response = $this->get("api/v4/meet/rooms");
- $response->assertStatus(401);
-
- // John has one room
- $response = $this->actingAs($john)->get("api/v4/meet/rooms");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertSame(1, $json['count']);
- $this->assertCount(1, $json['list']);
- $this->assertSame('john', $json['list'][0]['name']);
-
- // Jack has no room, but it will be auto-created
- $response = $this->actingAs($jack)->get("api/v4/meet/rooms");
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertSame(1, $json['count']);
- $this->assertCount(1, $json['list']);
- $this->assertMatchesRegularExpression('/^[0-9a-z-]{11}$/', $json['list'][0]['name']);
- }
-
/**
* Test joining the room
*
@@ -203,6 +170,16 @@
$json = $response->json();
$this->assertSame(Room::ROLE_PUBLISHER & $json['role'], 0);
+
+ // Test opening the session as a sharee of a room
+ $room = Room::where('name', 'jack')->first();
+ $response = $this->actingAs($jack)->post("api/v4/meet/rooms/{$room->name}", ['init' => 1]);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(Room::ROLE_SUBSCRIBER | Room::ROLE_MODERATOR | Room::ROLE_OWNER, $json['role']);
+ $this->assertMatchesRegularExpression('|^wss?://|', $json['token']);
}
/**
@@ -317,84 +294,6 @@
$this->assertMatchesRegularExpression('|^wss?://|', $json['token']);
}
- /**
- * Test configuring the room (session)
- *
- * @group meet
- */
- public function testSetRoomConfig(): void
- {
- $john = $this->getTestUser('john@kolab.org');
- $jack = $this->getTestUser('jack@kolab.org');
- $room = Room::where('name', 'john')->first();
-
- // Unauth access not allowed
- $response = $this->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(401);
-
- // Non-existing room name
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/non-existing/config", []);
- $response->assertStatus(404);
-
- // TODO: Test a room with a deleted owner
-
- // Non-owner
- $response = $this->actingAs($jack)->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(403);
-
- // Room owner
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", []);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
-
- // Set password and room lock
- $post = ['password' => 'aaa', 'locked' => 1];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
- $room->refresh();
- $this->assertSame('aaa', $room->getSetting('password'));
- $this->assertSame('true', $room->getSetting('locked'));
-
- // Unset password and room lock
- $post = ['password' => '', 'locked' => 0];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(200);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('success', $json['status']);
- $this->assertSame("Room configuration updated successfully.", $json['message']);
- $room->refresh();
- $this->assertSame(null, $room->getSetting('password'));
- $this->assertSame(null, $room->getSetting('locked'));
-
- // Test invalid option error
- $post = ['password' => 'eee', 'unknown' => 0];
- $response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}/config", $post);
- $response->assertStatus(422);
-
- $json = $response->json();
-
- $this->assertCount(2, $json);
- $this->assertSame('error', $json['status']);
- $this->assertSame("Invalid room configuration option.", $json['errors']['unknown']);
-
- $room->refresh();
- $this->assertSame(null, $room->getSetting('password'));
- }
-
/**
* Test the webhook
*
diff --git a/src/tests/Feature/Controller/Reseller/SkusTest.php b/src/tests/Feature/Controller/Reseller/SkusTest.php
--- a/src/tests/Feature/Controller/Reseller/SkusTest.php
+++ b/src/tests/Feature/Controller/Reseller/SkusTest.php
@@ -99,7 +99,7 @@
$json = $response->json();
- $this->assertCount(14, $json);
+ $this->assertCount(15, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
diff --git a/src/tests/Feature/Controller/RoomsTest.php b/src/tests/Feature/Controller/RoomsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/RoomsTest.php
@@ -0,0 +1,465 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Meet\Room;
+use Tests\TestCase;
+
+class RoomsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test deleting a room (DELETE /api/v4/rooms/<room-id>)
+ */
+ public function testDestroy(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $room = $this->getTestRoom('test', $john->wallets()->first());
+ $room->setConfig(['acl' => 'jack@kolab.org, full']);
+
+ // Unauth access not allowed
+ $response = $this->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->delete("api/v4/rooms/non-existing");
+ $response->assertStatus(404);
+
+ // Non-owner (sharee also can't delete the room)
+ $response = $this->actingAs($jack)->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->delete("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room deleted successfully.", $json['message']);
+ }
+
+ /**
+ * Test listing user rooms (GET /api/v4/rooms)
+ */
+ public function testIndex(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms");
+ $response->assertStatus(401);
+
+ // John has one room
+ $response = $this->actingAs($john)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame('jack', $json['list'][0]['name']);
+ $this->assertSame("Jack's room", $json['list'][0]['description']);
+ $this->assertSame('john', $json['list'][1]['name']);
+ $this->assertSame("John's room", $json['list'][1]['description']);
+
+ // Ned has no rooms, but is the John's wallet controller
+ $response = $this->actingAs($ned)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(2, $json['count']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame('jack', $json['list'][0]['name']);
+ $this->assertSame("Jack's room", $json['list'][0]['description']);
+ $this->assertSame('john', $json['list'][1]['name']);
+ $this->assertSame("John's room", $json['list'][1]['description']);
+
+ // Test Jack's list (he has no owned rooms, but John shares one with him)
+ $response = $this->actingAs($jack)->get("api/v4/rooms");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(1, $json['count']);
+ $this->assertCount(1, $json['list']);
+ $this->assertSame('jack', $json['list'][0]['name']);
+ $this->assertSame("Jack's room", $json['list'][0]['description']);
+ }
+
+ /**
+ * Test configuring the room (POST api/v4/rooms/<room-id>/config)
+ */
+ public function testSetConfig(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $room = $this->getTestRoom('test', $john->wallets()->first());
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->post("api/v4/rooms/non-existing/config", []);
+ $response->assertStatus(404);
+
+ // Non-owner
+ $response = $this->actingAs($jack)->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", []);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+
+ // Set password and room lock
+ $post = ['password' => 'aaa', 'locked' => 1];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+ $room->refresh();
+ $this->assertSame('aaa', $room->getSetting('password'));
+ $this->assertSame('true', $room->getSetting('locked'));
+
+ // Unset password and room lock
+ $post = ['password' => '', 'locked' => 0];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room configuration updated successfully.", $json['message']);
+ $this->assertSame(null, $room->getSetting('password'));
+ $this->assertSame(null, $room->getSetting('locked'));
+
+ // Test invalid option error
+ $post = ['password' => 'eee', 'unknown' => 0];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->name}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The requested configuration parameter is not supported.", $json['errors']['unknown']);
+
+ $room->refresh();
+ $this->assertSame('eee', $room->getSetting('password'));
+
+ // Test ACL
+ $post = ['acl' => ['jack@kolab.org, full', 'test, full', 'ned@kolab.org, read-only']];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertCount(2, $json['errors']['acl']);
+ $this->assertSame("The specified email address is invalid.", $json['errors']['acl'][1]);
+ $this->assertSame("The entry format is invalid. Expected an email address.", $json['errors']['acl'][2]);
+ $this->assertSame([], $room->getConfig()['acl']);
+
+ $post = ['acl' => ['jack@kolab.org, full']];
+ $response = $this->actingAs($john)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame(['jack@kolab.org, full'], $room->getConfig()['acl']);
+
+ // Acting as Jack
+ $post = ['password' => '123'];
+ $response = $this->actingAs($jack)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('123', $room->getConfig()['password']);
+
+ // Acting as Ned
+ $post = ['password' => '123456'];
+ $response = $this->actingAs($ned)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('123456', $room->getConfig()['password']);
+ }
+
+ /**
+ * Test getting a room info (GET /api/v4/rooms/<room-id>)
+ */
+ public function testShow(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $joe = $this->getTestUser('joe@kolab.org');
+ $wallet = $john->wallets()->first();
+ $room = $this->getTestRoom('test', $wallet, ['description' => 'desc']);
+ $room->setSettings(['password' => 'pass', 'locked' => 'true']);
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->get("api/v4/rooms/non-existing");
+ $response->assertStatus(404);
+
+ // Non-owner
+ $response = $this->actingAs($joe)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertSame('test', $json['name']);
+ $this->assertSame('desc', $json['description']);
+ $this->assertSame(false, $json['isDeleted']);
+ $this->assertTrue($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertTrue($json['canDelete']);
+ $this->assertCount(1, $json['skus']);
+ $this->assertSame([], $json['config']['acl']);
+ $this->assertSame('pass', $json['config']['password']);
+ $this->assertSame(true, $json['config']['locked']);
+ $this->assertSame($wallet->id, $json['wallet']['id']);
+ $this->assertSame($wallet->currency, $json['wallet']['currency']);
+ $this->assertSame($wallet->balance, $json['wallet']['balance']);
+
+ // Another wallet controller
+ $response = $this->actingAs($ned)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertTrue($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertTrue($json['canDelete']);
+
+ // Privilaged user
+ $room->setConfig(['acl' => ['jack@kolab.org, full']]);
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame($room->id, $json['id']);
+ $this->assertFalse($json['isOwner']);
+ $this->assertTrue($json['canUpdate']);
+ $this->assertFalse($json['canDelete']);
+ }
+
+ /**
+ * Test getting a room entitlements (GET /api/v4/rooms/<room-id>/skus)
+ */
+ public function testSkus(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $room = Room::where('name', 'john')->first();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(401);
+
+ // Non-existing room name
+ $response = $this->actingAs($john)->get("api/v4/rooms/non-existing/skus");
+ $response->assertStatus(404);
+
+ // Non-owner (the room is shared with Jack, but he should not see entitlements)
+ $response = $this->actingAs($jack)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(403);
+
+ // Room owner
+ $response = $this->actingAs($john)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame('room', $json[0]['title']);
+
+ // Room's wallet controller, not owner
+ $response = $this->actingAs($ned)->get("api/v4/rooms/{$room->id}/skus");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(1, $json);
+ $this->assertSame('room', $json[0]['title']);
+ }
+
+ /**
+ * Test creating a room (POST /api/v4/rooms)
+ */
+ public function testStore(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/rooms", []);
+ $response->assertStatus(401);
+
+ // Only wallet controllers can create rooms (for now)
+ $response = $this->actingAs($jack)->post("api/v4/rooms", []);
+ $response->assertStatus(403);
+
+ // Description too long
+ $post = ['description' => str_repeat('.', 192)];
+ $response = $this->actingAs($john)->post("api/v4/rooms", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The description may not be greater than 191 characters.", $json['errors']['description'][0]);
+
+ // Successful room creation
+ $post = ['description' => 'test123'];
+ $response = $this->actingAs($john)->post("api/v4/rooms", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room created successfully.", $json['message']);
+
+ $room = Room::where('description', $post['description'])->first();
+
+ $this->assertTrue($room->wallet()->id === $john->wallet()->id);
+
+ // Successful room creation (acting as a room controller)
+ $post = ['description' => 'test456'];
+ $response = $this->actingAs($ned)->post("api/v4/rooms", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room created successfully.", $json['message']);
+
+ $room = Room::where('description', $post['description'])->first();
+
+ $this->assertTrue($room->wallet()->id === $john->wallet()->id);
+ }
+
+ /**
+ * Test updating a room (PUT /api/v4/rooms</room-id>)
+ */
+ public function testUpdate(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+ $wallet = $john->wallets()->first();
+ $room = $this->getTestRoom('test', $wallet);
+
+ // Unauth access not allowed
+ $response = $this->put("api/v4/rooms/{$room->id}", []);
+ $response->assertStatus(401);
+
+ // Only wallet controllers can create rooms (for now)
+ $response = $this->actingAs($jack)->put("api/v4/rooms/{$room->id}", []);
+ $response->assertStatus(403);
+
+ // Description too long
+ $post = ['description' => str_repeat('.', 192)];
+ $response = $this->actingAs($john)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(422);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('error', $json['status']);
+ $this->assertSame("The description may not be greater than 191 characters.", $json['errors']['description'][0]);
+
+ // Successful room update (room owner)
+ $post = ['description' => '123'];
+ $response = $this->actingAs($john)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+
+ // Successful room update (acting as a room controller)
+ $post = ['description' => '456'];
+ $response = $this->actingAs($ned)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+
+ // Successful room update (acting as a sharee)
+ $room->setConfig(['acl' => 'jack@kolab.org, full']);
+ $post = ['description' => '789'];
+ $response = $this->actingAs($jack)->put("api/v4/rooms/{$room->id}", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame("Room updated successfully.", $json['message']);
+
+ $room->refresh();
+ $this->assertSame($post['description'], $room->description);
+ }
+}
diff --git a/src/tests/Feature/Controller/SkusTest.php b/src/tests/Feature/Controller/SkusTest.php
--- a/src/tests/Feature/Controller/SkusTest.php
+++ b/src/tests/Feature/Controller/SkusTest.php
@@ -105,7 +105,7 @@
$json = $response->json();
- $this->assertCount(14, $json);
+ $this->assertCount(15, $json);
$this->assertSame(100, $json[0]['prio']);
$this->assertSame($sku->id, $json[0]['id']);
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -171,7 +171,7 @@
}
/**
- * Test Entitlement::entitlementTitle()
+ * Test EntitleableTrait::toString()
*/
public function testEntitleableTitle(): void
{
@@ -202,24 +202,24 @@
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_mailbox->id)->first();
- $this->assertSame($user->email, $entitlement->entitleableTitle());
+ $this->assertSame($user->email, $entitlement->entitleable->toString());
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_group->id)->first();
- $this->assertSame($group->email, $entitlement->entitleableTitle());
+ $this->assertSame($group->email, $entitlement->entitleable->toString());
$entitlement = Entitlement::where('wallet_id', $wallet->id)
->where('sku_id', $sku_domain->id)->first();
- $this->assertSame($domain->namespace, $entitlement->entitleableTitle());
+ $this->assertSame($domain->namespace, $entitlement->entitleable->toString());
// Make sure it still works if the entitleable is deleted
$domain->delete();
$entitlement->refresh();
- $this->assertSame($domain->namespace, $entitlement->entitleableTitle());
+ $this->assertSame($domain->namespace, $entitlement->entitleable->toString());
$this->assertNotNull($entitlement->entitleable);
}
}
diff --git a/src/tests/Feature/MeetRoomTest.php b/src/tests/Feature/MeetRoomTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/MeetRoomTest.php
@@ -0,0 +1,123 @@
+<?php
+
+namespace Tests\Feature;
+
+use App\Meet\Room;
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class MeetRoomTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('room-test@' . \config('app.domain'));
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('room-test@' . \config('app.domain'));
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test room/user creation
+ */
+ public function testCreate(): void
+ {
+ Queue::fake();
+
+ // Test default room name generation
+ $room = new Room();
+ $room->save();
+
+ $this->assertMatchesRegularExpression('/^[0-9a-z]{3}-[0-9a-z]{3}-[0-9a-z]{3}$/', $room->name);
+
+ // Test keeping the specified room name
+ $room = new Room();
+ $room->name = 'test';
+ $room->save();
+
+ $this->assertSame('test', $room->name);
+ }
+
+ /**
+ * Test room/user deletion
+ */
+ public function testDelete(): void
+ {
+ Queue::fake();
+
+ $user = $this->getTestUser('room-test@' . \config('app.domain'));
+ $wallet = $user->wallets()->first();
+ $room = $this->getTestRoom('test', $wallet, [], [
+ 'password' => 'test',
+ 'acl' => ['john@kolab.org, full'],
+ ]);
+
+ $this->assertCount(1, $room->entitlements()->get());
+ $this->assertCount(1, $room->shares()->get());
+ $this->assertCount(1, $room->settings()->get());
+
+ // First delete the room, see if it deletes the room shares, entitlements and settings
+ $room->delete();
+
+ $this->assertTrue($room->fresh()->trashed());
+ $this->assertCount(0, $room->entitlements()->get());
+ $this->assertCount(0, $room->shares()->get());
+ $this->assertCount(1, $room->settings()->get());
+
+ $room->forceDelete();
+
+ $this->assertCount(0, Room::where('name', 'test')->get());
+ $this->assertCount(0, $room->settings()->get());
+
+ // Now test if deleting a user deletes the room
+ $room = $this->getTestRoom('test', $wallet, [], [
+ 'password' => 'test',
+ 'acl' => ['john@kolab.org, full'],
+ ]);
+
+ $user->delete();
+
+ $this->assertTrue($room->fresh()->trashed());
+ $this->assertCount(0, $room->entitlements()->get());
+ $this->assertCount(0, $room->shares()->get());
+ $this->assertCount(1, $room->settings()->get());
+ }
+
+ /**
+ * Test getConfig()/setConfig() (\App\Meet\RoomConfigTrait)
+ */
+ public function testConfig(): void
+ {
+ $room = $this->getTestRoom('test');
+
+ $room->setConfig($input = [
+ 'password' => 'test-pass',
+ 'nomedia' => true,
+ 'locked' => true,
+ 'acl' => ['john@kolab.org, full']
+ ]);
+
+ $config = $room->getConfig();
+
+ $this->assertCount(4, $config);
+ $this->assertSame($input['password'], $config['password']);
+ $this->assertSame($input['nomedia'], $config['nomedia']);
+ $this->assertSame($input['locked'], $config['locked']);
+ $this->assertSame($input['acl'], $config['acl']);
+
+ // Test input validation
+ }
+}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -11,6 +11,9 @@
class UserTest extends TestCase
{
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -26,6 +29,9 @@
$this->deleteTestDomain('UserAccountAdd.com');
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
\App\TenantSetting::truncate();
@@ -857,6 +863,14 @@
);
}
+ /**
+ * Test user deletion vs. rooms
+ */
+ public function testDeleteWithRooms(): void
+ {
+ $this->markTestIncomplete();
+ }
+
/**
* Tests for User::aliasExists()
*/
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -276,6 +276,22 @@
$this->assertTrue($bAccount->id === $aWallet->id);
}
+ /**
+ * Test Wallet::isController()
+ */
+ public function testIsController(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+ $jack = $this->getTestUser('jack@kolab.org');
+ $ned = $this->getTestUser('ned@kolab.org');
+
+ $wallet = $jack->wallet();
+
+ $this->assertTrue($wallet->isController($john));
+ $this->assertTrue($wallet->isController($ned));
+ $this->assertFalse($wallet->isController($jack));
+ }
+
/**
* Verify controllers can also be removed from wallets.
*/
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -326,6 +326,24 @@
$resource->forceDelete();
}
+ /**
+ * Delete a test room whatever it takes.
+ *
+ * @coversNothing
+ */
+ protected function deleteTestRoom($name)
+ {
+ Queue::fake();
+
+ $room = \App\Meet\Room::withTrashed()->where('name', $name)->first();
+
+ if (!$room) {
+ return;
+ }
+
+ $room->forceDelete();
+ }
+
/**
* Delete a test shared folder whatever it takes.
*
@@ -452,6 +470,27 @@
return $resource;
}
+ /**
+ * Get Room object by name, create it if needed.
+ *
+ * @coversNothing
+ */
+ protected function getTestRoom($name, $wallet = null, $attrib = [], $config = [])
+ {
+ $attrib['name'] = $name;
+ $room = \App\Meet\Room::create($attrib);
+
+ if ($wallet) {
+ $room->assignToWallet($wallet);
+ }
+
+ if (!empty($config)) {
+ $room->setConfig($config);
+ }
+
+ return $room;
+ }
+
/**
* Get SharedFolder object by email, create it if needed.
* Skip LDAP jobs.
diff --git a/src/tests/Unit/TransactionTest.php b/src/tests/Unit/TransactionTest.php
--- a/src/tests/Unit/TransactionTest.php
+++ b/src/tests/Unit/TransactionTest.php
@@ -132,11 +132,11 @@
$this->assertSame(13, $transactions[4]->amount);
$this->assertSame(Transaction::ENTITLEMENT_CREATED, $transactions[4]->type);
$this->assertSame(
- "test@test.com created mailbox for " . $ent->entitleableTitle(),
+ "test@test.com created mailbox for " . $ent->entitleable->toString(),
$transactions[4]->toString()
);
$this->assertSame(
- "Added mailbox for " . $ent->entitleableTitle(),
+ "Added mailbox for " . $ent->entitleable->toString(),
$transactions[4]->shortDescription()
);
@@ -144,11 +144,11 @@
$this->assertSame(14, $transactions[5]->amount);
$this->assertSame(Transaction::ENTITLEMENT_BILLED, $transactions[5]->type);
$this->assertSame(
- sprintf("%s for %s is billed at 0,14 CHF", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("%s for %s is billed at 0,14 CHF", $ent->sku->title, $ent->entitleable->toString()),
$transactions[5]->toString()
);
$this->assertSame(
- sprintf("Billed %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("Billed %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[5]->shortDescription()
);
@@ -156,11 +156,11 @@
$this->assertSame(15, $transactions[6]->amount);
$this->assertSame(Transaction::ENTITLEMENT_DELETED, $transactions[6]->type);
$this->assertSame(
- sprintf("test@test.com deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("test@test.com deleted %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[6]->toString()
);
$this->assertSame(
- sprintf("Deleted %s for %s", $ent->sku->title, $ent->entitleableTitle()),
+ sprintf("Deleted %s for %s", $ent->sku->title, $ent->entitleable->toString()),
$transactions[6]->shortDescription()
);
}

File Metadata

Mime Type
text/plain
Expires
Mon, Apr 6, 5:46 PM (5 h, 57 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18838613
Default Alt Text
D3584.1775497599.diff (141 KB)

Event Timeline