diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php --- a/src/app/Http/Controllers/API/V4/OpenViduController.php +++ b/src/app/Http/Controllers/API/V4/OpenViduController.php @@ -103,11 +103,12 @@ } $user = Auth::guard()->user(); + $isOwner = $user && $user->id == $room->user_id; // There's no existing session if (!$room->hasSession()) { // Participants can't join the room until the session is created by the owner - if (!$user || $user->id != $room->user_id) { + if (!$isOwner) { return $this->errorResponse(423, \trans('meet.session-not-found')); } @@ -123,6 +124,29 @@ } } + $password = (string) $room->getSetting('password'); + + $config = [ + 'locked' => $room->getSetting('locked') === 'true', + 'password' => $isOwner ? $password : '', + 'requires_password' => !$isOwner && strlen($password), + ]; + + // Validate room password + if (!$isOwner && strlen($password)) { + $request_password = request()->input('password'); + if ($request_password !== $password) { + // Note: We send the config to the client so it knows to display the password field + $response = [ + 'config' => $config, + 'message' => \trans('meet.session-password-error'), + 'status' => 'error', + ]; + + return response()->json($response, 425); + } + } + // Create session token for the current user/connection $response = $room->getSessionToken('PUBLISHER'); @@ -138,11 +162,74 @@ } // Tell the UI who's the room owner - $response['owner'] = $user && $user->id == $room->user_id; + $response['owner'] = $isOwner; + + // Append the room configuration + + $response['config'] = $config; return response()->json($response); } + /** + * 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) { + 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; + + 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 OpenVidu server * diff --git a/src/app/OpenVidu/Room.php b/src/app/OpenVidu/Room.php --- a/src/app/OpenVidu/Room.php +++ b/src/app/OpenVidu/Room.php @@ -5,6 +5,14 @@ use App\Traits\SettingsTrait; use Illuminate\Database\Eloquent\Model; +/** + * 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 OpenVidu session identifier + */ class Room extends Model { use SettingsTrait; diff --git a/src/resources/js/meet.js b/src/resources/js/meet.js --- a/src/resources/js/meet.js +++ b/src/resources/js/meet.js @@ -20,6 +20,7 @@ faMicrophone, faPowerOff, faUser, + faShieldAlt, faVideo, faVolumeMute } from '@fortawesome/free-solid-svg-icons' @@ -33,6 +34,7 @@ faMicrophone, faPowerOff, faUser, + faShieldAlt, faVideo, faVolumeMute ) 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 @@ -14,9 +14,13 @@ */ '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.', 'session-close-error' => 'Failed to close the session.', 'session-close-success' => 'The session has been closed successfully.', + 'session-password-error' => 'Failed to join the session. Invalid password.', + ]; diff --git a/src/resources/themes/forms.scss b/src/resources/themes/forms.scss --- a/src/resources/themes/forms.scss +++ b/src/resources/themes/forms.scss @@ -36,6 +36,36 @@ } } +.input-group-activable { + &.active { + :not(.input-group-append):not(.activable) { + display: none; + } + } + + &:not(.active) { + .activable { + display: none; + } + } + + // Label is always visible + .label { + color: $body-color; + display: initial !important; + } + + .input-group-text { + border-color: transparent; + background: transparent; + padding-left: 0; + + &:not(.label) { + flex: 1; + } + } +} + .form-control-plaintext .btn-sm { margin-top: -0.25rem; } diff --git a/src/resources/themes/meet.scss b/src/resources/themes/meet.scss --- a/src/resources/themes/meet.scss +++ b/src/resources/themes/meet.scss @@ -138,6 +138,10 @@ #meet-setup { max-width: 720px; + + .input-group svg { + width: 1em; + } } #meet-auth { diff --git a/src/resources/vue/Meet/Room.vue b/src/resources/vue/Meet/Room.vue --- a/src/resources/vue/Meet/Room.vue +++ b/src/resources/vue/Meet/Room.vue @@ -20,6 +20,9 @@ <button class="btn btn-link link-fullscreen open hidden" @click="switchFullscreen" title="Full screen"> <svg-icon icon="compress"></svg-icon> </button> + <button class="btn btn-link link-security" v-if="session && session.owner" @click="securityOptions" title="Security options"> + <svg-icon icon="shield-alt"></svg-icon> + </button> <button class="btn btn-link link-logout" @click="logout" title="Leave session"> <svg-icon icon="power-off"></svg-icon> </button> @@ -35,38 +38,52 @@ <video class="rounded"></video> <div class="volume"><div class="bar"></div></div> </div> - <div class="col-sm-6"> - <div class="form-group"> - <label for="setup-microphone">Microphone</label> + <div class="col-sm-6 align-self-center"> + <div class="input-group"> + <label for="setup-microphone" class="input-group-prepend mb-0"> + <span class="input-group-text" title="Microphone"><svg-icon icon="microphone"></svg-icon></span> + </label> <select class="custom-select" id="setup-microphone" v-model="microphone" @change="setupMicrophoneChange"> <option value="">None</option> <option v-for="mic in setup.microphones" :value="mic.deviceId" :key="mic.deviceId">{{ mic.label }}</option> </select> </div> - <div class="form-group"> - <label for="setup-camera">Camera</label> + <div class="input-group mt-2"> + <label for="setup-camera" class="input-group-prepend mb-0"> + <span class="input-group-text" title="Camera"><svg-icon icon="video"></svg-icon></span> + </label> <select class="custom-select" id="setup-camera" v-model="camera" @change="setupCameraChange"> <option value="">None</option> <option v-for="cam in setup.cameras" :value="cam.deviceId" :key="cam.deviceId">{{ cam.label }}</option> </select> </div> - <div class="form-group mb-0"> - <label for="setup-nickname">Nickname</label> - <input class="form-control" type="text" id="setup-nickname" v-model="nickname"> + <div class="input-group mt-2"> + <label for="setup-nickname" class="input-group-prepend mb-0"> + <span class="input-group-text" title="Nickname"><svg-icon icon="user"></svg-icon></span> + </label> + <input class="form-control" type="text" id="setup-nickname" v-model="nickname" placeholder="Your name"> + </div> + <div class="input-group mt-2" v-if="session.config && session.config.requires_password"> + <label for="setup-password" class="input-group-prepend mb-0"> + <span class="input-group-text" title="Password"><svg-icon icon="key"></svg-icon></span> + </label> + <input type="password" class="form-control" id="setup-password" v-model="password" placeholder="Password"> + </div> + <div class="mt-3"> + <button v-if="roomState == 'ready' || roomState == 424 || roomState == 425" + type="button" + @click="joinSession" + :class="'btn w-100 btn-' + (roomState == 'ready' ? 'success' : 'primary')" + >JOIN</button> + <button v-if="roomState == 423" + type="button" + @click="joinSession" + class="btn btn-primary w-100" + >I'm the owner</button> </div> </div> - <div class="text-center mt-4 col-sm-12"> - <status-message :status="roomState" :status-labels="roomStateLabels" class="mb-3"></status-message> - <button v-if="roomState == 'ready' || roomState == 424" - type="button" - @click="joinSession" - class="btn btn-primary pl-5 pr-5" - >JOIN</button> - <button v-if="roomState == 423" - type="button" - @click="joinSession" - class="btn btn-primary pl-5 pr-5" - >I'm the owner</button> + <div class="mt-4 col-sm-12"> + <status-message :status="roomState" :status-labels="roomStateLabels"></status-message> </div> </form> </div> @@ -103,6 +120,8 @@ </div> </div> </div> + + <session-security-options v-if="session.config" :config="session.config" :room="room" @config-update="configUpdate"></session-security-options> </div> </template> @@ -110,10 +129,12 @@ import Meet from '../../js/meet/app.js' import StatusMessage from '../Widgets/StatusMessage' import LogonForm from '../Login' + import SessionSecurityOptions from './SessionSecurityOptions' export default { components: { LogonForm, + SessionSecurityOptions, StatusMessage }, data() { @@ -127,6 +148,7 @@ meet: null, microphone: '', nickname: '', + password: '', room: null, roomState: 'init', roomStateLabels: { @@ -134,6 +156,7 @@ 404: 'The room does not exist.', 423: 'The room is closed. Please, wait for the owner to start the session.', 424: 'The room is closed. It will be open for others after you join.', + 425: 'The room is ready. Please, provide a valid password.', 500: 'Failed to create a session. Server error.' }, session: {} @@ -168,20 +191,20 @@ $('#meet-setup').removeClass('hidden') $('#meet-auth').addClass('hidden') }, + configUpdate(config) { + this.session.config = Object.assign({}, this.session.config, config) + }, initSession(init) { - let params = [] - - if (this.canShareScreen) { - params.push('screenShare=1') + this.post = { + password: this.password, + nickname: this.nickname, + screenShare: this.canShareScreen ? 1 : 0, + init: init ? 1 : 0 } - if (init) { - params.push('init=1') - } + $('#setup-password').removeClass('is-invalid') - const url = '/api/v4/openvidu/rooms/' + this.room + '?' + params.join('&') - - axios.get(url, { ignoreErrors: true }) + axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true }) .then(response => { // Response data contains: session, token and shareToken this.roomState = 'ready' @@ -194,10 +217,23 @@ .catch(error => { this.roomState = String(error.response.status) - // Waiting for the owner to open the room... - if (error.response.status == 423) { - // Update room state every 10 seconds - window.roomRequest = setTimeout(() => { this.initSession() }, 10000) + if (error.response.data && error.response.data.config) { + this.session.config = error.response.data.config + } + + switch (this.roomState) { + case '423': + // Waiting for the owner to open the room... + // Update room state every 10 seconds + window.roomRequest = setTimeout(() => { this.initSession() }, 10000) + break; + + case '425': + // Missing/invalid password + if (init) { + $('#setup-password').addClass('is-invalid').focus() + } + break; } }) @@ -212,7 +248,7 @@ return } - if (this.roomState == 424) { + if (this.roomState == 424 || this.roomState == 425) { this.initSession(true) return } @@ -223,6 +259,10 @@ $('#meet-setup').addClass('hidden') $('#meet-session-toolbar,#meet-session-layout').removeClass('hidden') + if (!this.canShareScreen) { + this.setMenuItem('screen', false, true) + } + this.session.nickname = this.nickname this.session.menuElement = $('#meet-session-menu')[0] this.session.chatElement = $('#meet-chat')[0] @@ -255,6 +295,9 @@ window.location = window.config['app.url'] } }, + securityOptions() { + $('#security-options-dialog').modal() + }, setMenuItem(type, state, disabled) { let button = $('#meet-session-menu').find('.link-' + type) @@ -338,7 +381,9 @@ // After one screen sharing session ended request a new token // for the next screen sharing session if (!enabled) { - axios.get('/api/v4/openvidu/rooms/' + this.room, { ignoreErrors: true }) + // TODO: This might need to be a different route. E.g. the room password might have + // changed since user joined the session + axios.post('/api/v4/openvidu/rooms/' + this.room, this.post, { ignoreErrors: true }) .then(response => { // Response data contains: session, token and shareToken this.session.shareToken = response.data.token diff --git a/src/resources/vue/Meet/SessionSecurityOptions.vue b/src/resources/vue/Meet/SessionSecurityOptions.vue new file mode 100644 --- /dev/null +++ b/src/resources/vue/Meet/SessionSecurityOptions.vue @@ -0,0 +1,110 @@ +<template> + <div v-if="config"> + <div id="security-options-dialog" class="modal" tabindex="-1" role="dialog"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title">Security options</h5> + <button type="button" class="close" data-dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <form id="security-options-password"> + <div id="password-input" class="input-group input-group-activable"> + <span class="input-group-text label">Password:</span> + <span v-if="config.password" id="password-input-text" class="input-group-text">{{ config.password }}</span> + <span v-else id="password-input-text" class="input-group-text text-muted">none</span> + <input type="text" :value="config.password" name="password" class="form-control rounded-left activable"> + <div class="input-group-append"> + <button type="button" @click="passwordSave" id="password-save-btn" class="btn btn-outline-primary activable rounded-right">Save</button> + <button type="button" v-if="config.password" id="password-clear-btn" @click="passwordClear" class="btn btn-outline-danger rounded">Clear password</button> + <button type="button" v-else @click="passwordSet" id="password-set-btn" class="btn btn-outline-primary rounded">Set password</button> + </div> + </div> + <small class="form-text text-muted"> + You can add a password to your meeting. Participants will have to provide + the password before they are allowed to join the meeting. + </small> + </form> + <hr v-if="false"> + <form v-if="false" id="security-options-lock"> + <div id="room-lock" class=""> + <span class="">Locked room:</span> + <input type="checkbox" name="lock" value="1" :checked="config.locked" @click="lockSave"> + </div> + <small class="form-text text-muted"> + When the room is locked participants have to be approved by you + before they could join the meeting. + </small> + </form> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-secondary modal-action" data-dismiss="modal">Close</button> + </div> + </div> + </div> + </div> + </div> +</template> + +<script> + export default { + props: { + config: { type: Object, default: () => null }, + room: { type: String, default: () => null } + }, + data() { + return { + } + }, + mounted() { + $('#security-options-dialog').on('show.bs.modal', e => { + $(e.target).find('.input-group-activable.active').removeClass('active') + }) + }, + methods: { + configSave(name, value, callback) { + const post = {} + post[name] = value + + axios.post('/api/v4/openvidu/rooms/' + this.room + '/config', post) + .then(response => { + this.config[name] = value + if (callback) { + callback(response.data) + } + this.$emit('config-update', this.config) + this.$toast.success(response.data.message) + }) + }, + lockSave(e) { + this.configSave('locked', $(e.target).prop('checked') ? 1 : 0) + }, + passwordClear() { + this.configSave('password', '') + }, + passwordSave() { + this.configSave('password', $('#password-input input').val(), () => { + $('#password-input').removeClass('active') + }) + }, + passwordSet() { + $('#password-input').addClass('active').find('input') + .off('keydown.pass') + .on('keydown.pass', e => { + if (e.which == 13) { + // On ENTER save the password + this.passwordSave() + e.preventDefault() + } else if (e.which == 27) { + // On ESC escape from the input, but not the dialog + $('#password-input').removeClass('active') + e.stopPropagation() + } + }) + .focus() + } + } + } +</script> diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -83,6 +83,7 @@ Route::get('openvidu/rooms', 'API\V4\OpenViduController@index'); Route::post('openvidu/rooms/{id}/close', 'API\V4\OpenViduController@closeRoom'); + Route::post('openvidu/rooms/{id}/config', 'API\V4\OpenViduController@setRoomConfig'); } ); @@ -93,7 +94,7 @@ 'prefix' => $prefix . 'api/v4' ], function () { - Route::get('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom'); + Route::post('openvidu/rooms/{id}', 'API\V4\OpenViduController@joinRoom'); } ); diff --git a/src/routes/websocket.php b/src/routes/websocket.php --- a/src/routes/websocket.php +++ b/src/routes/websocket.php @@ -15,27 +15,23 @@ Websocket::on( 'connect', function ($websocket, Request $request) { - \Log::debug("someone connected"); - $websocket->emit( - 'message', - 'welcome' - ); + return; } ); Websocket::on( 'open', function ($websocket, Request $request) { - \Log::debug("socket opened"); + return; } ); Websocket::on( 'disconnect', function ($websocket) { - \Log::debug("someone disconnected"); + return; } ); -Websocket::on('message', 'App\Http\Controllers\WebsocketController@message'); -Websocket::on('ping', 'App\Http\Controllers\WebsocketController@ping'); +//Websocket::on('message', 'App\Http\Controllers\WebsocketController@message'); +//Websocket::on('ping', 'App\Http\Controllers\WebsocketController@ping'); 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 @@ -124,9 +124,10 @@ $owner->assertToolbar([ 'audio' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'video' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, - 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, + 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, + 'security' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, ]) ->whenAvailable('div.meet-video.publisher', function (Browser $browser) { @@ -153,7 +154,7 @@ $guest->assertToolbar([ 'audio' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, 'video' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, - 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_DISABLED, + 'screen' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'chat' => RoomPage::BUTTON_INACTIVE | RoomPage::BUTTON_ENABLED, 'fullscreen' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, 'logout' => RoomPage::BUTTON_ACTIVE | RoomPage::BUTTON_ENABLED, diff --git a/src/tests/Browser/Meet/RoomSecurityTest.php b/src/tests/Browser/Meet/RoomSecurityTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Meet/RoomSecurityTest.php @@ -0,0 +1,129 @@ +<?php + +namespace Tests\Browser\Meet; + +use App\OpenVidu\Room; +use Tests\Browser; +use Tests\Browser\Components\Dialog; +use Tests\Browser\Components\Toast; +use Tests\Browser\Pages\Meet\Room as RoomPage; +use Tests\TestCaseDusk; + +class RoomSecurityTest extends TestCaseDusk +{ + /** + * {@inheritDoc} + */ + public function setUp(): void + { + parent::setUp(); + + $this->clearBetaEntitlements(); + $this->assignBetaEntitlement('john@kolab.org', 'meet'); + + $room = Room::where('name', 'john')->first(); + $room->setSettings(['password' => null, 'locked' => null]); + } + + public function tearDown(): void + { + $this->clearBetaEntitlements(); + $room = Room::where('name', 'john')->first(); + $room->setSettings(['password' => null, 'locked' => null]); + + parent::tearDown(); + } + + /** + * Test password protected room + * + * @group openvidu + */ + public function testRoomPassword(): void + { + $this->browse(function (Browser $owner, Browser $guest) { + // Make sure there's no session yet + $room = Room::where('name', 'john')->first(); + if ($room->session_id) { + $room->session_id = null; + $room->save(); + } + + // Join the room as an owner (authenticate) + $owner->visit(new RoomPage('john')) + ->click('@setup-button') + ->submitLogon('john@kolab.org', 'simple123') + ->waitFor('@setup-form') + ->waitUntilMissing('@setup-status-message.loading') + ->assertMissing('@setup-password-input') + ->click('@setup-button') + ->waitFor('@session') + // Enter Security option dialog + ->click('@menu button.link-security') + ->with(new Dialog('#security-options-dialog'), function (Browser $browser) use ($room) { + $browser->assertSeeIn('@title', 'Security options') + ->assertSeeIn('@button-action', 'Close') + ->assertElementsCount('.modal-footer button', 1) + ->assertSeeIn('#password-input .label', 'Password:') + ->assertSeeIn('#password-input-text.text-muted', 'none') + ->assertVisible('#password-input + small') + ->assertSeeIn('#password-set-btn', 'Set password') + ->assertElementsCount('#password-input button', 1) + ->assertMissing('#password-input input') + // Test setting a password + ->click('#password-set-btn') + ->assertMissing('#password-input-text') + ->assertVisible('#password-input input') + ->assertValue('#password-input input', '') + ->assertSeeIn('#password-input #password-save-btn', 'Save') + ->assertElementsCount('#password-input button', 1) + ->type('#password-input input', 'pass') + ->click('#password-input #password-save-btn') + ->assertToast(Toast::TYPE_SUCCESS, 'Room configuration updated successfully.') + ->assertMissing('#password-input input') + ->assertSeeIn('#password-input-text:not(.text-muted)', 'pass') + ->assertSeeIn('#password-clear-btn.btn-outline-danger', 'Clear password') + ->assertElementsCount('#password-input button', 1) + ->click('@button-action'); + + $this->assertSame('pass', $room->fresh()->getSetting('password')); + }); + + // In another browser act as a guest, expect password required + $guest->visit(new RoomPage('john')) + ->waitFor('@setup-form') + ->waitUntilMissing('@setup-status-message.loading') + ->assertSeeIn('@setup-status-message.text-danger', "Please, provide a valid password.") + ->assertVisible('@setup-form .input-group:nth-child(4) svg') + ->assertAttribute('@setup-form .input-group:nth-child(4) .input-group-text', 'title', 'Password') + ->assertAttribute('@setup-password-input', 'placeholder', 'Password') + ->assertValue('@setup-password-input', '') + ->assertSeeIn('@setup-button', "JOIN") + // Try to join w/o password + ->click('@setup-button') + ->waitFor('#setup-password.is-invalid') + // Try to join with a valid password + ->type('#setup-password', 'pass') + ->click('@setup-button') + ->waitFor('@session'); + + // Test removing the password + $owner->click('@menu button.link-security') + ->with(new Dialog('#security-options-dialog'), function (Browser $browser) use ($room) { + $browser->assertSeeIn('@title', 'Security options') + ->assertSeeIn('#password-input-text:not(.text-muted)', 'pass') + ->assertSeeIn('#password-clear-btn.btn-outline-danger', 'Clear password') + ->assertElementsCount('#password-input button', 1) + ->click('#password-clear-btn') + ->assertToast(Toast::TYPE_SUCCESS, 'Room configuration updated successfully.') + ->assertMissing('#password-input input') + ->assertSeeIn('#password-input-text.text-muted', 'none') + ->assertSeeIn('#password-set-btn', 'Set password') + ->assertElementsCount('#password-input button', 1) + ->click('@button-action'); + + $this->assertSame(null, $room->fresh()->getSetting('password')); + }); + }); + } +} 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 @@ -101,12 +101,17 @@ ->assertVisible('@setup-form') ->assertSeeIn('@setup-title', 'Set up your session') ->assertVisible('@setup-video') - ->assertSeeIn('@setup-form .form-group:nth-child(1) label', 'Microphone') + ->assertVisible('@setup-form .input-group:nth-child(1) svg') + ->assertAttribute('@setup-form .input-group:nth-child(1) .input-group-text', 'title', 'Microphone') ->assertVisible('@setup-mic-select') - ->assertSeeIn('@setup-form .form-group:nth-child(2) label', 'Camera') + ->assertVisible('@setup-form .input-group:nth-child(2) svg') + ->assertAttribute('@setup-form .input-group:nth-child(2) .input-group-text', 'title', 'Camera') ->assertVisible('@setup-cam-select') - ->assertSeeIn('@setup-form .form-group:nth-child(3) label', 'Nickname') + ->assertVisible('@setup-form .input-group:nth-child(3) svg') + ->assertAttribute('@setup-form .input-group:nth-child(3) .input-group-text', 'title', 'Nickname') ->assertValue('@setup-nickname-input', '') + ->assertAttribute('@setup-nickname-input', 'placeholder', 'Your name') + ->assertMissing('@setup-password-input') ->assertSeeIn( '@setup-status-message', "The room is closed. Please, wait for the owner to start the session." @@ -171,12 +176,12 @@ ->assertMissing('.status .status-video'); }) ->within(new Menu(), function ($browser) { - $browser->assertMenuItems(['explore', 'blog', 'support', 'logout']); + $browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout']); }); if ($browser->isDesktop()) { $browser->within(new Menu('footer'), function ($browser) { - $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'logout']); + $browser->assertMenuItems(['explore', 'blog', 'support', 'tos', 'dashboard', 'logout']); }); } diff --git a/src/tests/Browser/Pages/Meet/Room.php b/src/tests/Browser/Pages/Meet/Room.php --- a/src/tests/Browser/Pages/Meet/Room.php +++ b/src/tests/Browser/Pages/Meet/Room.php @@ -63,6 +63,7 @@ '@setup-mic-select' => '#setup-microphone', '@setup-cam-select' => '#setup-camera', '@setup-nickname-input' => '#setup-nickname', + '@setup-password-input' => '#setup-password', '@setup-preview' => '#setup-preview', '@setup-volume' => '#setup-preview .volume', '@setup-video' => '#setup-preview video', diff --git a/src/tests/Feature/Controller/OpenViduTest.php b/src/tests/Feature/Controller/OpenViduTest.php --- a/src/tests/Feature/Controller/OpenViduTest.php +++ b/src/tests/Feature/Controller/OpenViduTest.php @@ -14,12 +14,18 @@ public function setUp(): void { parent::setUp(); + $this->clearBetaEntitlements(); + $room = Room::where('name', 'john')->first(); + $room->setSettings(['password' => null, 'locked' => null]); } public function tearDown(): void { $this->clearBetaEntitlements(); + $room = Room::where('name', 'john')->first(); + $room->setSettings(['password' => null, 'locked' => null]); + parent::tearDown(); } @@ -75,22 +81,22 @@ $this->assignBetaEntitlement($john, 'meet'); // Unauth access, no session yet - $response = $this->get("api/v4/openvidu/rooms/{$room->name}"); + $response = $this->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(423); // Non-existing room name - $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/non-existing"); + $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing"); $response->assertStatus(404); // Non-owner, no session yet - $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}"); + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(423); // Room owner, no session yet - $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}"); + $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(424); - $response = $this->actingAs($john)->get("api/v4/openvidu/rooms/{$room->name}?init=1"); + $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}", ['init' => 1]); $response->assertStatus(200); $json = $response->json(); @@ -106,7 +112,7 @@ $john_token = $json['token']; // Non-owner, now the session exists - $response = $this->actingAs($jack)->get("api/v4/openvidu/rooms/{$room->name}"); + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); $response->assertStatus(200); $json = $response->json(); @@ -116,6 +122,37 @@ $this->assertTrue(strpos($json['token'], 'wss://') === 0); $this->assertTrue($json['token'] != $john_token); $this->assertTrue(!array_key_exists('shareToken', $json)); + $this->assertEmpty($json['config']['password']); + $this->assertEmpty($json['config']['requires_password']); + + // Non-owner, password protected room, password not provided + $room->setSettings(['password' => 'pass']); + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}"); + $response->assertStatus(425); + + $json = $response->json(); + + $this->assertCount(3, $json); + $this->assertSame('error', $json['status']); + $this->assertSame('Failed to join the session. Invalid password.', $json['message']); + $this->assertEmpty($json['config']['password']); + $this->assertTrue($json['config']['requires_password']); + + // Non-owner, password protected room, invalid provided + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['password' => 'aa']); + $response->assertStatus(425); + + // Non-owner, password protected room, valid password provided + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}", ['password' => 'pass']); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame($session_id, $json['session']); + + // Make sure the room owner can access the password protected room w/o password + $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/{$room->name}"); + $response->assertStatus(200); // TODO: Test accessing an existing room of deleted owner } @@ -135,7 +172,7 @@ $room = Room::where('name', 'john')->first(); // Guest, request with screenShare token - $response = $this->get("api/v4/openvidu/rooms/{$room->name}?screenShare=1"); + $response = $this->post("api/v4/openvidu/rooms/{$room->name}", ['screenShare' => 1]); $response->assertStatus(200); $json = $response->json(); @@ -199,4 +236,82 @@ $this->assertSame("Failed to close the session.", $json['message']); $this->assertCount(2, $json); } + + /** + * Test configuring the room (session) + * + * @group openvidu + */ + 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/openvidu/rooms/{$room->name}/config", []); + $response->assertStatus(401); + + // Non-existing room name + $response = $this->actingAs($john)->post("api/v4/openvidu/rooms/non-existing/config", []); + $response->assertStatus(404); + + // TODO: Test a room with a deleted owner + + // Non-owner + $response = $this->actingAs($jack)->post("api/v4/openvidu/rooms/{$room->name}/config", []); + $response->assertStatus(403); + + // Room owner + $response = $this->actingAs($john)->post("api/v4/openvidu/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/openvidu/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/openvidu/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/openvidu/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')); + } }