Page MenuHomePhorge

D3584.1775427881.diff
No OneTemporary

Authored By
Unknown
Size
186 KB
Referenced Files
None
Subscribers
None

D3584.1775427881.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/Meet.php b/src/app/Handlers/Room.php
rename from src/app/Handlers/Meet.php
rename to src/app/Handlers/Room.php
--- a/src/app/Handlers/Meet.php
+++ b/src/app/Handlers/Room.php
@@ -2,7 +2,7 @@
namespace App\Handlers;
-class Meet extends Base
+class Room extends Base
{
/**
* The entitleable class for this handler.
@@ -11,7 +11,7 @@
*/
public static function entitleableClass(): string
{
- return \App\User::class;
+ return \App\Meet\Room::class;
}
/**
@@ -25,19 +25,9 @@
{
$data = parent::metadata($sku);
- $data['required'] = ['Groupware'];
+ $data['readonly'] = true;
+ $data['enabled'] = true;
return $data;
}
-
- /**
- * The priority that specifies the order of SKUs in UI.
- * Higher number means higher on the list.
- *
- * @return int
- */
- public static function priority(): int
- {
- return 50;
- }
}
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,12 @@
$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, \trans('meet.room-not-found'));
- }
-
- // Check if there's still a valid meet entitlement for the room owner
- if (!$room->owner->hasSku('meet')) {
+ if (!$room || !($wallet = $room->wallet()) || !$wallet->owner || $wallet->owner->isDegraded(true)) {
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 +138,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,292 @@
+<?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, $share);
+ if (is_int($room)) {
+ return $this->errorResponse($room);
+ }
+
+ $request = request()->input();
+
+ // Room sharees can't manage room ACL
+ if ($share) {
+ unset($request['acl']);
+ }
+
+ $errors = $room->setConfig($request);
+
+ 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, $share);
+ 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();
+
+ // Room sharees can't manage/see room ACL
+ if ($share) {
+ unset($response['config']['acl']);
+ }
+
+ $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
+ * @param ?\App\Share $share Room share reference if the user has permissions to the room and is not the owner
+ *
+ * @return \App\Meet\Room|int File object or error code
+ */
+ protected function inputRoom($id, $rights = 0, &$share = null): 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,
'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,9 @@
}
});
}
+
+ // TODO: Remove Share records for the user
+ // TODO: Remove file permissions for the user
}
/**
@@ -263,6 +266,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,57 @@
+<?php
+
+namespace App;
+
+use App\Traits\UuidStrKeyTrait;
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * The eloquent definition of a Share.
+ *
+ * @property string $id Share identifier
+ * @property int $rights Access rights
+ * @property int $shareable_id The shared object identifier
+ * @property string $shareable_type The shared object type (class name)
+ * @property string $user Permitted user (email)
+ */
+class Share extends Model
+{
+ use UuidStrKeyTrait;
+
+ public const READ = 1;
+ public const WRITE = 2;
+ public 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,163 @@
+<?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])) {
+ $errors[$i] = \trans('validation.acl-permission-invalid');
+ } elseif (in_array($user, $users) || ($error = $this->validateACLIdentifier($user))) {
+ $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,139 @@
+<?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,
+ ]);
+ }
+ }
+ }
+
+ // Remove 'meet' SKU/entitlements
+ \App\Sku::where('title', 'meet')->delete();
+
+ 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();
+
+ \App\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,
+ ]);
+
+ 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,25 @@
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]->setConfig(['acl' => 'jack@kolab.org, full']);
}
}
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
@@ -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' => true,
- ]
- );
-
- 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,86 +119,48 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Activesync',
'active' => true,
- ]
- );
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'beta', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- 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
- $sku = Sku::where(['title' => 'meet', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- 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
- $sku = Sku::where(['title' => 'group', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- 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
- $sku = Sku::where(['title' => 'beta-distlists', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create(
- [
- 'title' => 'beta-distlists',
- 'name' => 'Distribution lists',
- 'description' => 'Access to mail distribution lists',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Beta\Distlists',
- 'active' => true,
- ]
- );
- }
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'beta-resources', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- 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' => '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',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Beta\Distlists',
+ 'active' => true,
+ ],
+ [
'title' => 'beta-resources',
'name' => 'Calendaring resources',
'description' => 'Access to calendaring resources',
@@ -237,14 +169,8 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\Resources',
'active' => true,
- ]);
- }
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'beta-shared-folders', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create([
+ ],
+ [
'title' => 'beta-shared-folders',
'name' => 'Shared folders',
'description' => 'Access to shared folders',
@@ -253,14 +179,8 @@
'period' => 'monthly',
'handler_class' => 'App\Handlers\Beta\SharedFolders',
'active' => true,
- ]);
- }
-
- // Check existence because migration might have added this already
- $sku = Sku::where(['title' => 'files', 'tenant_id' => \config('app.tenant_id')])->first();
-
- if (!$sku) {
- Sku::create([
+ ],
+ [
'title' => 'files',
'name' => 'File storage',
'description' => 'Access to file storage',
@@ -269,112 +189,92 @@
'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'])->where('tenant_id', \config('app.tenant_id'))->first()) {
+ Sku::create($sku);
+ }
}
+ $skus = [
+ [
+ 'title' => 'mailbox',
+ 'name' => 'User Mailbox',
+ 'description' => 'Just a mailbox',
+ 'cost' => 500,
+ 'fee' => 333,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Mailbox',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'storage',
+ 'name' => 'Storage Quota',
+ 'description' => 'Some wiggle room',
+ 'cost' => 25,
+ 'fee' => 16,
+ 'units_free' => 5,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Storage',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'domain-hosting',
+ 'name' => 'External Domain',
+ 'description' => 'Host a domain that is externally registered',
+ 'cost' => 100,
+ 'fee' => 66,
+ 'units_free' => 1,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\DomainHosting',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'groupware',
+ 'name' => 'Groupware Features',
+ 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
+ 'cost' => 490,
+ 'fee' => 327,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Groupware',
+ 'active' => true,
+ ],
+ [
+ 'title' => '2fa',
+ 'name' => '2-Factor Authentication',
+ 'description' => 'Two factor authentication for webmail and administration panel',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Auth2F',
+ 'active' => true,
+ ],
+ [
+ 'title' => 'activesync',
+ 'name' => 'Activesync',
+ 'description' => 'Mobile synchronization',
+ 'cost' => 0,
+ 'units_free' => 0,
+ 'period' => 'monthly',
+ 'handler_class' => 'App\Handlers\Activesync',
+ 'active' => true,
+ ],
+ ];
+
// for tenants that are not the configured tenant id
$tenants = \App\Tenant::where('id', '!=', \config('app.tenant_id'))->get();
foreach ($tenants as $tenant) {
- $sku = Sku::create(
- [
- 'title' => 'mailbox',
- 'name' => 'User Mailbox',
- 'description' => 'Just a mailbox',
- 'cost' => 500,
- 'fee' => 333,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Mailbox',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'storage',
- 'name' => 'Storage Quota',
- 'description' => 'Some wiggle room',
- 'cost' => 25,
- 'fee' => 16,
- 'units_free' => 5,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Storage',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'domain-hosting',
- 'name' => 'External Domain',
- 'description' => 'Host a domain that is externally registered',
- 'cost' => 100,
- 'fee' => 66,
- 'units_free' => 1,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\DomainHosting',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'groupware',
- 'name' => 'Groupware Features',
- 'description' => 'Groupware functions like Calendar, Tasks, Notes, etc.',
- 'cost' => 490,
- 'fee' => 327,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Groupware',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => '2fa',
- 'name' => '2-Factor Authentication',
- 'description' => 'Two factor authentication for webmail and administration panel',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Auth2F',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
-
- $sku = Sku::create(
- [
- 'title' => 'activesync',
- 'name' => 'Activesync',
- 'description' => 'Mobile synchronization',
- 'cost' => 0,
- 'units_free' => 0,
- 'period' => 'monthly',
- 'handler_class' => 'App\Handlers\Activesync',
- 'active' => true,
- ]
- );
-
- $sku->tenant_id = $tenant->id;
- $sku->save();
+ foreach ($skus as $sku) {
+ $sku = Sku::create($sku);
+ $sku->tenant_id = $tenant->id;
+ $sku->save();
+ }
}
}
}
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,38 @@
'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' => '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 +159,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 +169,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 +179,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 +189,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/meet/room.js b/src/resources/js/meet/room.js
--- a/src/resources/js/meet/room.js
+++ b/src/resources/js/meet/room.js
@@ -739,7 +739,6 @@
// TODO: This probably could be better done with css
let elements = {
- '.dropdown-menu': withMenu,
'.permissions': withPerm,
'.interpreting': withPerm && rolePublisher,
'svg.moderator': roleModerator,
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,31 +215,6 @@
],
'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.",
- 'security' => "Room Security",
- 'security-text' => "Increase the room security by setting a password that attendees will need to know"
- . " before they can enter, or lock the door so attendees will have to knock, and a moderator can accept or deny those requests.",
- 'qa-title' => "Raise Hand (Q&A)",
- 'qa-text' => "Silent audience members can raise their hand to facilitate a Question & Answer session with the panel members.",
- 'moderation' => "Moderator Delegation",
- 'moderation-text' => "Delegate moderator authority for the session, so that a speaker is not needlessly"
- . " interrupted with attendees knocking and other moderator duties.",
- 'eject' => "Eject Attendees",
- 'eject-text' => "Eject attendees from the session in order to force them to reconnect, or address policy"
- . " violations. Click the user icon for effective dismissal.",
- 'silent' => "Silent Audience Members",
- 'silent-text' => "For a webinar-style session, configure the room to force all new attendees to be silent audience members.",
- 'interpreters' => "Language Specific Audio Channels",
- 'interpreters-text' => "Designate a participant to interpret the original audio to a target language, for sessions"
- . " with multi-lingual attendees. The interpreter is expected to be able to relay the original audio, and override it.",
- 'beta-notice' => "Keep in mind that this is still in beta and might come with some issues."
- . " Should you encounter any on your way, let us know by contacting support.",
-
// Room options dialog
'options' => "Room options",
'password' => "Password",
@@ -354,6 +330,22 @@
'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-empty-nocontroller' => "Do you need a room? Ask your account owner to create one and share it with you.",
+ '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 +482,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/en/validation.php b/src/resources/lang/en/validation.php
--- a/src/resources/lang/en/validation.php
+++ b/src/resources/lang/en/validation.php
@@ -153,6 +153,7 @@
'spf-entry-invalid' => 'The entry format is invalid. Expected a domain name starting with a dot.',
'sp-entry-invalid' => 'The entry format is invalid. Expected an email, domain, or part of it.',
'acl-entry-invalid' => 'The entry format is invalid. Expected an email address.',
+ 'acl-permission-invalid' => 'The specified permission is invalid.',
'file-perm-exists' => 'File permission already exists.',
'file-perm-invalid' => 'The file permission is invalid.',
'file-name-exists' => 'The file name already exists.',
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 v-if="room.isOwner" 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,81 @@
+<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: []
+ }
+ },
+ computed: {
+ setup() {
+ let setup = {
+ buttons: true,
+ model: 'room',
+ columns: [
+ {
+ prop: 'name',
+ icon: 'comments',
+ link: true
+ },
+ {
+ prop: 'description',
+ link: true
+ }
+ ]
+ }
+
+ if (!this.$root.hasPermission('settings')) {
+ setup.footLabel = 'room.list-empty-nocontroller'
+ }
+
+ return setup
+ }
+ },
+ 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/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php
--- a/src/tests/Browser/Admin/UserTest.php
+++ b/src/tests/Browser/Admin/UserTest.php
@@ -40,7 +40,6 @@
Entitlement::where('cost', '>=', 5000)->delete();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
}
/**
@@ -62,7 +61,6 @@
Entitlement::where('cost', '>=', 5000)->delete();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
parent::tearDown();
}
diff --git a/src/tests/Browser/Meet/RoomControlsTest.php b/src/tests/Browser/Meet/RoomControlsTest.php
--- a/src/tests/Browser/Meet/RoomControlsTest.php
+++ b/src/tests/Browser/Meet/RoomControlsTest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomInterpretersTest.php b/src/tests/Browser/Meet/RoomInterpretersTest.php
--- a/src/tests/Browser/Meet/RoomInterpretersTest.php
+++ b/src/tests/Browser/Meet/RoomInterpretersTest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomModeratorTest.php b/src/tests/Browser/Meet/RoomModeratorTest.php
--- a/src/tests/Browser/Meet/RoomModeratorTest.php
+++ b/src/tests/Browser/Meet/RoomModeratorTest.php
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomOptionsTest.php b/src/tests/Browser/Meet/RoomOptionsTest.php
--- a/src/tests/Browser/Meet/RoomOptionsTest.php
+++ b/src/tests/Browser/Meet/RoomOptionsTest.php
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
diff --git a/src/tests/Browser/Meet/RoomQATest.php b/src/tests/Browser/Meet/RoomQATest.php
--- a/src/tests/Browser/Meet/RoomQATest.php
+++ b/src/tests/Browser/Meet/RoomQATest.php
@@ -15,7 +15,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
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
@@ -17,7 +17,7 @@
public function setUp(): void
{
parent::setUp();
- $this->setupTestRoom();
+ $this->resetTestRoom();
}
public function tearDown(): void
@@ -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,8 @@
public function setUp(): void
{
parent::setUp();
- $this->clearMeetEntitlements();
+
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
}
/**
@@ -27,14 +30,15 @@
*/
public function tearDown(): void
{
- $this->clearMeetEntitlements();
+ Room::withTrashed()->whereNotIn('name', ['jack', 'john'])->forceDelete();
+ $room = $this->resetTestRoom('jack');
+ $room->setConfig(['acl' => ['jack@kolab.org, full']]);
+
parent::tearDown();
}
/**
- * Test rooms page (unauthenticated and unauthorized)
- *
- * @group meet
+ * Test rooms page (unauthenticated)
*/
public function testRoomsUnauth(): void
{
@@ -42,58 +46,250 @@
$this->browse(function (Browser $browser) {
$browser->visit('/rooms')
->on(new Home())
- // User has no 'meet' entitlement yet, expect redirect to error page
->submitLogon('john@kolab.org', 'simple123', false)
- ->waitFor('#app > #error-page')
- ->assertSeeIn('#error-page .code', '403')
- ->assertSeeIn('#error-page .message', 'Access denied');
+ ->on(new RoomList());
});
}
/**
- * 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
$browser->visit('/login')
->on(new Home())
->submitLogon('john@kolab.org', 'simple123', true)
->on(new Dashboard())
- ->assertMissing('@links a.link-chat');
+ ->assertSeeIn('@links a.link-chat', 'Video chat')
+ // Test Video chat page
+ ->click('@links a.link-chat')
+ ->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);
- // Goto user subscriptions, and enable 'meet' subscription
- $browser->visit('/user/' . $john->id)
- ->on(new UserInfo())
- ->whenAvailable('@skus', function ($browser) {
- $browser->click('#sku-input-meet');
+ $browser->on(new RoomPage('john'))
+ // 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');
+ });
+ }
+
+ /**
+ * Test rooms create and edit and delete
+ */
+ public function testRoomCreateAndEditAndDelete(): void
+ {
+ $john = $this->getTestUser('john@kolab.org');
+
+ $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');
})
- ->scrollTo('#general button[type=submit]')->pause(200)
- ->click('#general button[type=submit]')
- ->assertToast(Toast::TYPE_SUCCESS, 'User data updated successfully.')
- ->click('.navbar-brand')
- ->on(new Dashboard())
- ->assertSeeIn('@links a.link-chat', 'Video chat')
- // Make sure the element also exists on Dashboard page load
- ->refresh()
+ ->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');
+ $john = $this->getTestUser('john@kolab.org');
+ $room = $this->resetTestRoom('jack');
+ $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())
- ->assertSeeIn('@links a.link-chat', 'Video chat');
-
- // 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'))
+ ->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')
+ ->assertMissing('.row:nth-child(4)')
+ ->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(['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')
@@ -105,7 +301,8 @@
->assertSeeIn('@setup-button', "JOIN")
->click('@setup-button')
->waitFor('@session')
- ->assertMissing('@setup-form');
+ ->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->waitForLocation($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/Browser/Reseller/UserTest.php b/src/tests/Browser/Reseller/UserTest.php
--- a/src/tests/Browser/Reseller/UserTest.php
+++ b/src/tests/Browser/Reseller/UserTest.php
@@ -38,7 +38,6 @@
$wallet->save();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
}
/**
@@ -59,7 +58,6 @@
$wallet->save();
$this->deleteTestGroup('group-test@kolab.org');
- $this->clearMeetEntitlements();
parent::tearDown();
}
diff --git a/src/tests/Browser/UsersTest.php b/src/tests/Browser/UsersTest.php
--- a/src/tests/Browser/UsersTest.php
+++ b/src/tests/Browser/UsersTest.php
@@ -55,7 +55,6 @@
$wallet->save();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -79,10 +78,10 @@
$wallet = $john->wallets()->first();
$wallet->discount()->dissociate();
+ $wallet->currency = 'CHF';
$wallet->save();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -201,7 +200,7 @@
$browser->assertSeeIn('div.row:nth-child(8) label', 'Subscriptions')
->assertVisible('@skus.row:nth-child(8)')
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 6)
+ $browser->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.name', 'User Mailbox')
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 CHF/month')
@@ -251,15 +250,6 @@
'tbody tr:nth-child(5) td.buttons button',
'Two factor authentication for webmail and administration panel'
)
- // Meet SKU
- ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
- ->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
- ->assertNotChecked('tbody tr:nth-child(6) td.selection input')
- ->assertEnabled('tbody tr:nth-child(6) td.selection input')
- ->assertTip(
- 'tbody tr:nth-child(6) td.buttons button',
- 'Video conferencing tool'
- )
->click('tbody tr:nth-child(4) td.selection input');
})
->assertMissing('@skus table + .hint')
@@ -288,11 +278,6 @@
->assertDialogOpened('Activesync requires Groupware Features.')
->acceptDialog()
->assertNotChecked('#sku-input-activesync')
- // Check 'meet', expect an alert
- ->click('#sku-input-meet')
- ->assertDialogOpened('Voice & Video Conferencing (public beta) requires Groupware Features.')
- ->acceptDialog()
- ->assertNotChecked('#sku-input-meet')
// Check '2FA', expect 'activesync' unchecked and readonly
->click('#sku-input-2fa')
->assertChecked('#sku-input-2fa')
@@ -639,7 +624,7 @@
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 6)
+ ->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '4,50 CHF/month¹')
// Storage SKU
@@ -648,7 +633,7 @@
$browser->setQuotaValue(100);
})
->assertSeeIn('tr:nth-child(2) td.price', '21,37 CHF/month¹')
- // groupware SKU
+ // Groupware SKU
->assertSeeIn('tbody tr:nth-child(3) td.price', '4,41 CHF/month¹')
// ActiveSync SKU
->assertSeeIn('tbody tr:nth-child(4) td.price', '0,00 CHF/month¹')
@@ -703,7 +688,7 @@
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
// Beta SKU
- ->assertSeeIn('tbody tr:nth-child(7) td.price', '45,09 CHF/month¹')
+ ->assertSeeIn('tbody tr:nth-child(6) td.price', '45,09 CHF/month¹')
// Storage SKU
->assertSeeIn('tr:nth-child(2) td.price', '45,00 CHF/month¹')
->with($quota_input, function (Browser $browser) {
@@ -754,7 +739,7 @@
$browser->whenAvailable('@skus', function (Browser $browser) {
$quota_input = new QuotaInput('tbody tr:nth-child(2) .range-input');
$browser->waitFor('tbody tr')
- ->assertElementsCount('tbody tr', 6)
+ ->assertElementsCount('tbody tr', 5)
// Mailbox SKU
->assertSeeIn('tbody tr:nth-child(1) td.price', '5,00 €/month')
// Storage SKU
@@ -797,61 +782,52 @@
$browser->visit('/user/' . $john->id)
->on(new UserInfo())
->with('@skus', function ($browser) {
- $browser->assertElementsCount('tbody tr', 11)
- // Meet SKU
- ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Voice & Video Conferencing (public beta)')
- ->assertSeeIn('tr:nth-child(6) td.price', '0,00 CHF/month')
- ->assertNotChecked('tbody tr:nth-child(6) td.selection input')
+ $browser->assertElementsCount('tbody tr', 10)
+ // Beta SKU
+ ->assertSeeIn('tbody tr:nth-child(6) td.name', 'Private Beta (invitation only)')
+ ->assertSeeIn('tbody tr:nth-child(6) td.price', '0,00 CHF/month')
+ ->assertChecked('tbody tr:nth-child(6) td.selection input')
->assertEnabled('tbody tr:nth-child(6) td.selection input')
->assertTip(
'tbody tr:nth-child(6) td.buttons button',
- 'Video conferencing tool'
+ 'Access to the private beta program subscriptions'
)
- // Beta SKU
- ->assertSeeIn('tbody tr:nth-child(7) td.name', 'Private Beta (invitation only)')
- ->assertSeeIn('tbody tr:nth-child(7) td.price', '0,00 CHF/month')
- ->assertChecked('tbody tr:nth-child(7) td.selection input')
+ // Distlists SKU
+ ->assertSeeIn('tbody tr:nth-child(7) td.name', 'Distribution lists')
+ ->assertSeeIn('tr:nth-child(7) td.price', '0,00 CHF/month')
+ ->assertNotChecked('tbody tr:nth-child(7) td.selection input')
->assertEnabled('tbody tr:nth-child(7) td.selection input')
->assertTip(
'tbody tr:nth-child(7) td.buttons button',
- 'Access to the private beta program subscriptions'
+ 'Access to mail distribution lists'
)
- // Distlists SKU
- ->assertSeeIn('tbody tr:nth-child(8) td.name', 'Distribution lists')
+ // Resources SKU
+ ->assertSeeIn('tbody tr:nth-child(8) td.name', 'Calendaring resources')
->assertSeeIn('tr:nth-child(8) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(8) td.selection input')
->assertEnabled('tbody tr:nth-child(8) td.selection input')
->assertTip(
'tbody tr:nth-child(8) td.buttons button',
- 'Access to mail distribution lists'
+ 'Access to calendaring resources'
)
- // Resources SKU
- ->assertSeeIn('tbody tr:nth-child(9) td.name', 'Calendaring resources')
+ // Shared folders SKU
+ ->scrollTo('tbody tr:nth-child(9)')->pause(250)
+ ->assertSeeIn('tbody tr:nth-child(9) td.name', 'Shared folders')
->assertSeeIn('tr:nth-child(9) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(9) td.selection input')
->assertEnabled('tbody tr:nth-child(9) td.selection input')
->assertTip(
'tbody tr:nth-child(9) td.buttons button',
- 'Access to calendaring resources'
+ 'Access to shared folders'
)
- // Shared folders SKU
+ // Files SKU
->scrollTo('tbody tr:nth-child(10)')->pause(250)
- ->assertSeeIn('tbody tr:nth-child(10) td.name', 'Shared folders')
+ ->assertSeeIn('tbody tr:nth-child(10) td.name', 'File storage')
->assertSeeIn('tr:nth-child(10) td.price', '0,00 CHF/month')
->assertNotChecked('tbody tr:nth-child(10) td.selection input')
->assertEnabled('tbody tr:nth-child(10) td.selection input')
->assertTip(
'tbody tr:nth-child(10) td.buttons button',
- 'Access to shared folders'
- )
- // Files SKU
- ->scrollTo('tbody tr:nth-child(11)')->pause(250)
- ->assertSeeIn('tbody tr:nth-child(11) td.name', 'File storage')
- ->assertSeeIn('tr:nth-child(11) td.price', '0,00 CHF/month')
- ->assertNotChecked('tbody tr:nth-child(11) td.selection input')
- ->assertEnabled('tbody tr:nth-child(11) td.selection input')
- ->assertTip(
- 'tbody tr:nth-child(11) td.buttons button',
'Access to file storage'
)
// Check Distlist, Uncheck Beta, expect Distlist unchecked
diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php
--- a/src/tests/Browser/WalletTest.php
+++ b/src/tests/Browser/WalletTest.php
@@ -25,7 +25,7 @@
$this->deleteTestUser('wallets-controller@kolabnow.com');
$john = $this->getTestUser('john@kolab.org');
- Wallet::where('user_id', $john->id)->update(['balance' => -1234]);
+ Wallet::where('user_id', $john->id)->update(['balance' => -1234, 'currency' => 'CHF']);
}
/**
diff --git a/src/tests/Feature/Console/Sku/ListUsersTest.php b/src/tests/Feature/Console/Sku/ListUsersTest.php
--- a/src/tests/Feature/Console/Sku/ListUsersTest.php
+++ b/src/tests/Feature/Console/Sku/ListUsersTest.php
@@ -34,7 +34,7 @@
{
// Warning: We're not using artisan() here, as this will not
// allow us to test "empty output" cases
- $code = \Artisan::call('sku:list-users meet');
+ $code = \Artisan::call('sku:list-users domain-registration');
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
$this->assertSame('', $output);
@@ -68,21 +68,5 @@
$output = trim(\Artisan::output());
$this->assertSame(0, $code);
$this->assertSame("john@kolab.org", $output);
-
- $sku = \App\Sku::where('title', 'meet')->first();
- $user = $this->getTestUser('sku-list-users@kolabnow.com');
- $user->assignSku($sku);
-
- $code = \Artisan::call('sku:list-users meet');
- $output = trim(\Artisan::output());
- $this->assertSame(0, $code);
- $this->assertSame($user->email, $output);
-
- $user->assignSku($sku);
-
- $code = \Artisan::call('sku:list-users meet');
- $output = trim(\Artisan::output());
- $this->assertSame(0, $code);
- $this->assertSame($user->email, $output);
}
}
diff --git a/src/tests/Feature/Console/User/AssignSkuTest.php b/src/tests/Feature/Console/User/AssignSkuTest.php
--- a/src/tests/Feature/Console/User/AssignSkuTest.php
+++ b/src/tests/Feature/Console/User/AssignSkuTest.php
@@ -31,7 +31,7 @@
*/
public function testHandle(): void
{
- $sku = \App\Sku::where('title', 'meet')->first();
+ $sku = \App\Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$user = $this->getTestUser('add-entitlement@kolabnow.com');
$this->artisan('user:assign-sku unknown@unknown.org ' . $sku->id)
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
@@ -18,7 +18,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -29,7 +28,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -118,7 +116,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
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
@@ -15,56 +15,21 @@
{
parent::setUp();
- $this->clearMeetEntitlements();
$room = Room::where('name', 'john')->first();
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
- $this->clearMeetEntitlements();
$room = Room::where('name', 'john')->first();
$room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
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
*
@@ -78,8 +43,6 @@
$room->session_id = null;
$room->save();
- $this->assignMeetEntitlement($john);
-
// Unauth access, no session yet
$response = $this->post("api/v4/meet/rooms/{$room->name}");
$response->assertStatus(422);
@@ -203,6 +166,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']);
}
/**
@@ -219,8 +192,6 @@
$room->save();
$room->setSettings(['password' => null, 'locked' => 'true']);
- $this->assignMeetEntitlement($john);
-
// Create the session (also makes sure the owner can access a locked room)
$response = $this->actingAs($john)->post("api/v4/meet/rooms/{$room->name}", ['init' => 1]);
$response->assertStatus(200);
@@ -300,8 +271,6 @@
*/
public function testJoinRoomGuest(): void
{
- $this->assignMeetEntitlement('john@kolab.org');
-
// There's no easy way to logout the user in the same test after
// using actingAs(). That's why this is moved to a separate test
$room = Room::where('name', 'john')->first();
@@ -317,84 +286,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
*
@@ -402,8 +293,6 @@
*/
public function testWebhook(): void
{
- $this->assignMeetEntitlement('john@kolab.org');
-
$john = $this->getTestUser('john@kolab.org');
$room = Room::where('name', 'john')->first();
$headers = ['X-Auth-Token' => \config('meet.webhook_token')];
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
@@ -18,7 +18,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
}
/**
@@ -29,7 +28,6 @@
Sku::where('title', 'test')->delete();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
parent::tearDown();
}
@@ -167,7 +165,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
// Note: Details are tested where we test API\V4\SkusController
}
}
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,468 @@
+<?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 specified permission is invalid.", $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', 'acl' => ['joe@kolab.org, full']];
+ $response = $this->actingAs($jack)->post("api/v4/rooms/{$room->id}/config", $post);
+ $response->assertStatus(200);
+
+ $this->assertSame('123', $room->getConfig()['password']);
+ $this->assertSame(['jack@kolab.org, full'], $room->getConfig()['acl']);
+
+ // 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']);
+
+ // Privileged 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']);
+ $this->assertSame('pass', $json['config']['password']);
+ $this->assertTrue(empty($json['config']['acl']));
+ }
+
+ /**
+ * 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
@@ -18,7 +18,6 @@
parent::setUp();
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
}
@@ -28,7 +27,6 @@
public function tearDown(): void
{
$this->clearBetaEntitlements();
- $this->clearMeetEntitlements();
Sku::where('title', 'test')->delete();
parent::tearDown();
@@ -149,7 +147,7 @@
$json = $response->json();
- $this->assertCount(6, $json);
+ $this->assertCount(5, $json);
$this->assertSkuElement('mailbox', $json[0], [
'prio' => 100,
@@ -198,15 +196,6 @@
'forbidden' => ['Activesync'],
]);
- $this->assertSkuElement('meet', $json[5], [
- 'prio' => 50,
- 'type' => 'user',
- 'handler' => 'Meet',
- 'enabled' => false,
- 'readonly' => false,
- 'required' => ['Groupware'],
- ]);
-
// Test inclusion of beta SKUs
$sku = Sku::withEnvTenantContext()->where('title', 'beta')->first();
$user->assignSku($sku);
@@ -215,9 +204,9 @@
$json = $response->json();
- $this->assertCount(11, $json);
+ $this->assertCount(10, $json);
- $this->assertSkuElement('beta', $json[6], [
+ $this->assertSkuElement('beta', $json[5], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta',
@@ -225,7 +214,7 @@
'readonly' => false,
]);
- $this->assertSkuElement('beta-distlists', $json[7], [
+ $this->assertSkuElement('beta-distlists', $json[6], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta\Distlists',
@@ -234,7 +223,7 @@
'required' => ['Beta'],
]);
- $this->assertSkuElement('beta-resources', $json[8], [
+ $this->assertSkuElement('beta-resources', $json[7], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta\Resources',
@@ -243,7 +232,7 @@
'required' => ['Beta'],
]);
- $this->assertSkuElement('beta-shared-folders', $json[9], [
+ $this->assertSkuElement('beta-shared-folders', $json[8], [
'prio' => 10,
'type' => 'user',
'handler' => 'Beta\SharedFolders',
@@ -252,7 +241,7 @@
'required' => ['Beta'],
]);
- $this->assertSkuElement('files', $json[10], [
+ $this->assertSkuElement('files', $json[9], [
'prio' => 10,
'type' => 'user',
'handler' => 'Files',
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -509,17 +509,11 @@
$this->assertSame(['beta'], $result['skus']);
- $user->assignSku(Sku::withEnvTenantContext()->where('title', 'meet')->first());
+ $user->assignSku(Sku::withEnvTenantContext()->where('title', 'groupware')->first());
$result = UsersController::statusInfo($user);
- $this->assertSame(['beta', 'meet'], $result['skus']);
-
- $user->assignSku(Sku::withEnvTenantContext()->where('title', 'meet')->first());
-
- $result = UsersController::statusInfo($user);
-
- $this->assertSame(['beta', 'meet'], $result['skus']);
+ $this->assertSame(['beta', 'groupware'], $result['skus']);
}
/**
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,130 @@
+<?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
+ $result = $room->setConfig($input = [
+ 'acl' => ['jack@kolab.org, read-only', 'test@unknown.org, full'],
+ ]);
+
+ $this->assertCount(2, $result['acl']);
+ $this->assertSame("The specified permission is invalid.", $result['acl'][0]);
+ $this->assertSame("The specified email address does not exist.", $result['acl'][1]);
+ }
+}
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/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -8,7 +8,6 @@
abstract class TestCase extends BaseTestCase
{
use TestCaseTrait;
- use TestCaseMeetTrait;
/**
* {@inheritDoc}
diff --git a/src/tests/TestCaseDusk.php b/src/tests/TestCaseDusk.php
--- a/src/tests/TestCaseDusk.php
+++ b/src/tests/TestCaseDusk.php
@@ -10,7 +10,6 @@
abstract class TestCaseDusk extends BaseTestCase
{
use TestCaseTrait;
- use TestCaseMeetTrait;
/**
* Prepare for Dusk test execution.
diff --git a/src/tests/TestCaseMeetTrait.php b/src/tests/TestCaseMeetTrait.php
deleted file mode 100644
--- a/src/tests/TestCaseMeetTrait.php
+++ /dev/null
@@ -1,56 +0,0 @@
-<?php
-
-namespace Tests;
-
-use App\Meet\Room;
-
-trait TestCaseMeetTrait
-{
- /**
- * Assign 'meet' entitlement to a user.
- *
- * @param string|\App\User $user The user
- */
- protected function assignMeetEntitlement($user): void
- {
- if (is_string($user)) {
- $user = $this->getTestUser($user);
- }
-
- $user->assignSku(\App\Sku::where('title', 'meet')->first());
- }
-
- /**
- * Removes all 'meet' entitlements from the database
- */
- protected function clearMeetEntitlements(): void
- {
- $meet_sku = \App\Sku::where('title', 'meet')->first();
- \App\Entitlement::where('sku_id', $meet_sku->id)->delete();
- }
-
- /**
- * Reset a room after tests
- */
- public function resetTestRoom($room_name = 'john'): void
- {
- $this->clearMeetEntitlements();
-
- $room = Room::where('name', $room_name)->first();
- $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
-
- if ($room->session_id) {
- $room->session_id = null;
- $room->save();
- }
- }
-
- /**
- * Prepare a room for testing
- */
- public function setupTestRoom($room_name = 'john'): void
- {
- $this->resetTestRoom($room_name);
- $this->assignMeetEntitlement('john@kolab.org');
- }
-}
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.
@@ -568,6 +607,22 @@
];
}
+ /**
+ * Reset a room after tests
+ */
+ public function resetTestRoom(string $room_name = 'john')
+ {
+ $room = \App\Meet\Room::where('name', $room_name)->first();
+ $room->setSettings(['password' => null, 'locked' => null, 'nomedia' => null]);
+
+ if ($room->session_id) {
+ $room->session_id = null;
+ $room->save();
+ }
+
+ return $room;
+ }
+
protected function setUpTest()
{
$this->userPassword = \App\Utils::generatePassphrase();
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
Sun, Apr 5, 10:24 PM (22 h, 8 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18834784
Default Alt Text
D3584.1775427881.diff (186 KB)

Event Timeline