From d875ed77b02a0b6deda8d8050acfe9b7c9ca512e Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:27:42 +0200 Subject: [PATCH 01/15] Add privacy setting to disable finding users by partial matches of their name --- .../Controllers/api/v1/SettingsController.php | 1 + .../Controllers/api/v1/UserController.php | 9 ++++-- app/Http/Requests/UpdateSettingsRequest.php | 1 + app/Http/Resources/ConfigResource.php | 1 + app/Http/Resources/SettingsResource.php | 1 + app/Settings/UserSettings.php | 2 ++ ...5_130614_add_user_name_search_disabled.php | 18 +++++++++++ lang/en/admin.php | 4 +++ lang/en/rooms.php | 2 ++ .../RoomTabMembersAddSingleModal.vue | 21 +++++++++++-- .../RoomTransferOwnershipButton.vue | 21 +++++++++++-- resources/js/views/AdminSettings.vue | 30 +++++++++++++++++++ 12 files changed, 103 insertions(+), 8 deletions(-) create mode 100644 database/settings/2026_07_15_130614_add_user_name_search_disabled.php diff --git a/app/Http/Controllers/api/v1/SettingsController.php b/app/Http/Controllers/api/v1/SettingsController.php index bff8927df5..c33a375e2e 100644 --- a/app/Http/Controllers/api/v1/SettingsController.php +++ b/app/Http/Controllers/api/v1/SettingsController.php @@ -189,6 +189,7 @@ public function update(UpdateSettingsRequest $request) $roomSettings->hide_owner_from_guests = $request->boolean('room_hide_owner_from_guests'); $userSettings->password_change_allowed = $request->boolean('user_password_change_allowed'); + $userSettings->search_by_name = $request->boolean('user_search_by_name'); $bannerSettings->enabled = $request->boolean('banner_enabled'); $bannerSettings->title = $request->input('banner_title'); diff --git a/app/Http/Controllers/api/v1/UserController.php b/app/Http/Controllers/api/v1/UserController.php index c25ad099fb..20e7f48d19 100644 --- a/app/Http/Controllers/api/v1/UserController.php +++ b/app/Http/Controllers/api/v1/UserController.php @@ -19,6 +19,7 @@ use App\Services\AuthenticationService; use App\Services\EmailVerification\EmailVerificationService; use App\Settings\GeneralSettings; +use App\Settings\UserSettings; use Carbon\Carbon; use Exception; use Illuminate\Auth\Access\AuthorizationException; @@ -46,13 +47,17 @@ public function __construct() * @param UserSearchRequest $request query parameter with search query * @return AnonymousResourceCollection */ - public function search(UserSearchRequest $request) + public function search(UserSearchRequest $request, UserSettings $userSettings) { if (! $request->filled('query')) { abort(204, 'Too many results'); } - $query = User::withNameOrEmail($request->query('query')); + if ($userSettings->search_by_name) { + $query = User::withNameOrEmail($request->query('query')); + } else { + $query = User::whereLike('email', $request->query('query')); + } if ($query->count() > config('bigbluebutton.user_search_limit')) { abort(204, 'Too many results'); diff --git a/app/Http/Requests/UpdateSettingsRequest.php b/app/Http/Requests/UpdateSettingsRequest.php index 9ae7803d04..6091d97259 100644 --- a/app/Http/Requests/UpdateSettingsRequest.php +++ b/app/Http/Requests/UpdateSettingsRequest.php @@ -75,6 +75,7 @@ public function rules() 'room_hide_owner_from_guests' => ['required', 'boolean'], 'user_password_change_allowed' => ['required', 'boolean'], + 'user_search_by_name' => ['required', 'boolean'], 'recording_server_usage_enabled' => ['required', 'boolean'], 'recording_server_usage_retention_period' => ['required', 'numeric', Rule::enum(TimePeriod::class)], diff --git a/app/Http/Resources/ConfigResource.php b/app/Http/Resources/ConfigResource.php index dd529a9c1d..c9236037c9 100644 --- a/app/Http/Resources/ConfigResource.php +++ b/app/Http/Resources/ConfigResource.php @@ -69,6 +69,7 @@ public function toArray($request) ], 'user' => [ 'password_change_allowed' => $userSettings->password_change_allowed, + 'search_by_name' => $userSettings->search_by_name, ], 'bbb' => [ 'file_mimes' => config('bigbluebutton.allowed_file_mimes'), diff --git a/app/Http/Resources/SettingsResource.php b/app/Http/Resources/SettingsResource.php index 52f1b58800..5935cf606d 100644 --- a/app/Http/Resources/SettingsResource.php +++ b/app/Http/Resources/SettingsResource.php @@ -70,6 +70,7 @@ public function toArray($request) 'room_file_terms_of_use' => $roomSettings->file_terms_of_use, 'room_hide_owner_from_guests' => $roomSettings->hide_owner_from_guests, 'user_password_change_allowed' => $userSettings->password_change_allowed, + 'user_search_by_name' => $userSettings->search_by_name, 'recording_server_usage_enabled' => $recordingSettings->server_usage_enabled, 'recording_server_usage_retention_period' => $recordingSettings->server_usage_retention_period, 'recording_meeting_usage_enabled' => $recordingSettings->meeting_usage_enabled, diff --git a/app/Settings/UserSettings.php b/app/Settings/UserSettings.php index 4a5c2313e4..8ebca456e3 100644 --- a/app/Settings/UserSettings.php +++ b/app/Settings/UserSettings.php @@ -10,6 +10,8 @@ class UserSettings extends Settings { public bool $password_change_allowed; + public bool $search_by_name; + public static function group(): string { return 'user'; diff --git a/database/settings/2026_07_15_130614_add_user_name_search_disabled.php b/database/settings/2026_07_15_130614_add_user_name_search_disabled.php new file mode 100644 index 0000000000..d7be415db3 --- /dev/null +++ b/database/settings/2026_07_15_130614_add_user_name_search_disabled.php @@ -0,0 +1,18 @@ +migrator->add('user.search_by_name', true); + } + + public function down(): void + { + $this->migrator->delete('user.search_by_name'); + } +}; diff --git a/lang/en/admin.php b/lang/en/admin.php index 776656a683..adbadfb7c7 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -426,6 +426,10 @@ ], 'two_weeks' => '2 Weeks (14 Days)', 'two_years' => '2 Years (730 Days)', + 'user_search_by_name' => [ + 'description' => 'Allows users to find others by partial matches of their name or email address. When disabled, users can only be found by entering their exact email address.', + 'title' => 'Allow searching users by name', + ], 'user_settings' => 'User settings', ], 'streaming' => [ diff --git a/lang/en/rooms.php b/lang/en/rooms.php index 238a8a87f9..1aa159746a 100644 --- a/lang/en/rooms.php +++ b/lang/en/rooms.php @@ -222,8 +222,10 @@ 'add' => [ 'add' => 'Add', 'no_options' => 'Enter the name or email address of a user.', + 'no_options_email_only' => 'Enter the email address of a user.', 'no_result' => 'Oops! No user was found for this query.', 'placeholder' => 'Name or email', + 'placeholder_email_only' => 'Email', 'select_role' => 'Please select a role', 'select_user' => 'Please select the user you would like to add', 'too_many_results' => 'Too many users were found. Please enter a more precise search term.', diff --git a/resources/js/components/RoomTabMembersAddSingleModal.vue b/resources/js/components/RoomTabMembersAddSingleModal.vue index 038740d52b..dc6ef4b1e9 100644 --- a/resources/js/components/RoomTabMembersAddSingleModal.vue +++ b/resources/js/components/RoomTabMembersAddSingleModal.vue @@ -46,7 +46,11 @@ data-test="select-user-dropdown" label="lastname" track-by="id" - :placeholder="$t('rooms.members.modals.add.placeholder')" + :placeholder=" + settingsStore.getSetting('user.search_by_name') + ? $t('rooms.members.modals.add.placeholder') + : $t('rooms.members.modals.add.placeholder_email_only') + " open-direction="bottom" :options="users" :multiple="false" @@ -62,7 +66,7 @@ :show-no-results="true" :show-labels="false" :class="{ 'is-invalid': formErrors.fieldInvalid('user') }" - @search-change="asyncFind" + @search-change="onSearchChange" > @@ -73,7 +77,11 @@ - {{ $t("rooms.members.modals.add.no_options") }} + {{ + settingsStore.getSetting("user.search_by_name") + ? $t("rooms.members.modals.add.no_options") + : $t("rooms.members.modals.add.no_options_email_only") + }} {{ option.firstname }} {{ option.lastname }}{{ @@ -149,6 +157,8 @@ import { useApi } from "../composables/useApi.js"; import { useFormErrors } from "../composables/useFormErrors.js"; import { ref } from "vue"; import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from "../constants/httpStatusCodes.js"; +import { useDebounceFn } from "@vueuse/core"; +import { useSettingsStore } from "../stores/settings.js"; const props = defineProps({ roomId: { @@ -163,6 +173,7 @@ const props = defineProps({ const emit = defineEmits(["added"]); +const settingsStore = useSettingsStore(); const api = useApi(); const formErrors = useFormErrors(); @@ -178,6 +189,10 @@ defineExpose({ showModal, }); +const onSearchChange = useDebounceFn((query) => { + asyncFind(query); +}, 500); + /** * Search for users in database * @param query diff --git a/resources/js/components/RoomTransferOwnershipButton.vue b/resources/js/components/RoomTransferOwnershipButton.vue index 897bdc1cd5..f7bbab9e3c 100644 --- a/resources/js/components/RoomTransferOwnershipButton.vue +++ b/resources/js/components/RoomTransferOwnershipButton.vue @@ -39,7 +39,11 @@ :disabled="isLoadingAction" label="lastname" track-by="id" - :placeholder="$t('app.user_name')" + :placeholder=" + settingsStore.getSetting('user.search_by_name') + ? $t('rooms.members.modals.add.placeholder') + : $t('rooms.members.modals.add.placeholder_email_only') + " open-direction="bottom" :options="users" :multiple="false" @@ -54,7 +58,7 @@ :show-no-results="true" :show-labels="false" :class="{ 'is-invalid': formErrors.fieldInvalid('user') }" - @search-change="asyncFind" + @search-change="onSearchChange" > @@ -65,7 +69,11 @@ - {{ $t("rooms.members.modals.add.no_options") }} + {{ + settingsStore.getSetting("user.search_by_name") + ? $t("rooms.members.modals.add.no_options") + : $t("rooms.members.modals.add.no_options_email_only") + }} {{ option.firstname }} {{ option.lastname }}{{ @@ -188,6 +196,8 @@ import { useApi } from "../composables/useApi.js"; import { ref } from "vue"; import { useUserPermissions } from "../composables/useUserPermission.js"; import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from "../constants/httpStatusCodes.js"; +import { useDebounceFn } from "@vueuse/core"; +import { useSettingsStore } from "../stores/settings.js"; const props = defineProps({ room: { @@ -210,6 +220,7 @@ const users = ref([]); const newOwner = ref(null); const newRoleInRoom = ref(3); +const settingsStore = useSettingsStore(); const formErrors = useFormErrors(); const api = useApi(); const userPermissions = useUserPermissions(); @@ -268,6 +279,10 @@ function showModal() { modalVisible.value = true; } +const onSearchChange = useDebounceFn((query) => { + asyncFind(query); +}, 500); + /** * Search for users in database * @param query diff --git a/resources/js/views/AdminSettings.vue b/resources/js/views/AdminSettings.vue index 96cc6c5886..ee779f2e92 100644 --- a/resources/js/views/AdminSettings.vue +++ b/resources/js/views/AdminSettings.vue @@ -1151,6 +1151,36 @@ /> + + + + {{ $t("admin.settings.user_search_by_name.title") }} + + + + + {{ + $t("app.enable") + }} + + {{ + $t("admin.settings.user_search_by_name.description") + }} + + + Date: Thu, 16 Jul 2026 16:06:34 +0200 Subject: [PATCH 02/15] Adjust backend tests --- tests/Backend/Feature/api/v1/SettingsTest.php | 20 ++++++++++ tests/Backend/Feature/api/v1/UserTest.php | 39 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/Backend/Feature/api/v1/SettingsTest.php b/tests/Backend/Feature/api/v1/SettingsTest.php index d1c99cf7a3..12454c2951 100644 --- a/tests/Backend/Feature/api/v1/SettingsTest.php +++ b/tests/Backend/Feature/api/v1/SettingsTest.php @@ -14,6 +14,7 @@ use App\Settings\GeneralSettings; use App\Settings\RoomSettings; use App\Settings\ThemeSettings; +use App\Settings\UserSettings; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Contracts\Filesystem\FileNotFoundException; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -89,6 +90,7 @@ public function test_view_settings() $this->roomSettings->save(); $this->userSettings->password_change_allowed = true; + $this->userSettings->user_search_by_name = true; $this->userSettings->save(); $this->recordingSettings->server_usage_enabled = true; @@ -164,6 +166,7 @@ public function test_view_settings() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => true, + 'user_search_by_name' => true, 'recording_server_usage_enabled' => true, 'recording_server_usage_retention_period' => 7, @@ -228,6 +231,7 @@ public function test_update_settings() 'room_hide_owner_from_guests' => true, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -292,6 +296,7 @@ public function test_update_settings() 'room_hide_owner_from_guests' => true, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -309,12 +314,14 @@ public function test_update_settings() $this->assertEquals('http://localhost', app(GeneralSettings::class)->legal_notice_url); $this->assertEquals('http://localhost', app(GeneralSettings::class)->privacy_policy_url); $this->assertEquals('http://localhost', app(GeneralSettings::class)->accessibility_statement_url); + $this->assertTrue(app(UserSettings::class)->search_by_name); $payload['general_help_url'] = ''; $payload['general_legal_notice_url'] = ''; $payload['general_privacy_policy_url'] = ''; $payload['general_accessibility_statement_url'] = ''; $payload['room_file_terms_of_use'] = ''; + $payload['user_search_by_name'] = 0; $payload['bbb_default_welcome_message'] = ''; $this->putJson(route('api.v1.settings.update'), $payload) @@ -325,6 +332,7 @@ public function test_update_settings() $this->assertNull(app(GeneralSettings::class)->privacy_policy_url); $this->assertNull(app(GeneralSettings::class)->accessibility_statement_url); $this->assertNull(app(RoomSettings::class)->file_terms_of_use); + $this->assertFalse(app(UserSettings::class)->search_by_name); $this->assertNull(app(BigBlueButtonSettings::class)->default_welcome_message); } @@ -373,6 +381,7 @@ public function test_update_with_valid_inputs_image_file() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -452,6 +461,7 @@ public function test_update_with_valid_inputs_image_file_and_url() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -535,6 +545,7 @@ public function test_update_with_invalid_inputs() 'room_hide_owner_from_guests' => 'notbool', 'user_password_change_allowed' => 'foo', + 'user_search_by_name' => 'foo', 'recording_server_usage_enabled' => 'foo', 'recording_server_usage_retention_period' => 'notnumber', @@ -589,6 +600,7 @@ public function test_update_with_invalid_inputs() 'room_hide_owner_from_guests', 'user_password_change_allowed', + 'user_search_by_name', 'recording_server_usage_enabled', 'recording_server_usage_retention_period', @@ -673,6 +685,7 @@ public function test_update_theme_custom_css() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -780,6 +793,7 @@ public function test_update_min_max() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 1, @@ -835,6 +849,7 @@ public function test_update_min_max() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 366, @@ -917,6 +932,7 @@ public function test_update_default_presentation() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -1029,6 +1045,7 @@ public function test_update_bbb_style() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -1126,6 +1143,7 @@ public function test_update_bbb_logo() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -1210,6 +1228,7 @@ public function test_update_bbb_dark_logo() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, @@ -1300,6 +1319,7 @@ public function test_virus_files() 'room_hide_owner_from_guests' => false, 'user_password_change_allowed' => 1, + 'user_search_by_name' => 1, 'recording_server_usage_enabled' => 0, 'recording_server_usage_retention_period' => 7, diff --git a/tests/Backend/Feature/api/v1/UserTest.php b/tests/Backend/Feature/api/v1/UserTest.php index 12e4ad94aa..19a1f38602 100644 --- a/tests/Backend/Feature/api/v1/UserTest.php +++ b/tests/Backend/Feature/api/v1/UserTest.php @@ -14,6 +14,7 @@ use App\Notifications\PasswordReset; use App\Notifications\UserWelcome; use App\Notifications\VerifyEmail; +use App\Settings\UserSettings; use Carbon\Carbon; use Database\Seeders\RolesAndPermissionsSeeder; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -198,6 +199,10 @@ public function test_search() $searchLimit = 5; config(['bigbluebutton.user_search_limit' => $searchLimit]); + $userSettings = app(UserSettings::class); + $userSettings->search_by_name = true; + $userSettings->save(); + $users = []; $users[] = User::factory()->create(['firstname' => 'Gregory', 'lastname' => 'Dumas', 'email' => 'gregory.dumas@example.com']); $users[] = User::factory()->create(['firstname' => 'Mable', 'lastname' => 'Torres', 'email' => 'mable.torres@example.com']); @@ -254,6 +259,40 @@ public function test_search() ->assertJsonCount(1, 'data'); } + public function test_search_search_by_name_disabled() + { + $searchLimit = 5; + config(['bigbluebutton.user_search_limit' => $searchLimit]); + + $userSettings = app(UserSettings::class); + $userSettings->search_by_name = false; + $userSettings->save(); + + $users = []; + $users[] = User::factory()->create(['firstname' => 'Gregory', 'lastname' => 'Dumas', 'email' => 'gregory.dumas@example.com']); + + // Check with exact lastname match (search for names should not be possible) + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Dumas') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Check with exact lastname match (search for names should not be possible) + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Gregory') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Check fragments of email (query must match exactly, fragments are disabled) + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=example.com') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Check exact email (case-insensitive) + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Gregory.Dumas@example.com') + ->assertSuccessful() + ->assertJsonPath('data.0.firstname', $users[0]->firstname) + ->assertJsonCount(1, 'data'); + } + public function test_create() { $user = User::factory()->create(); From 0f68e1aa31ab3300d5a6246b81a7cab3a19c76ab Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:46:59 +0200 Subject: [PATCH 03/15] Adjust frontend tests --- tests/Frontend/e2e/AdminSettingsEdit.cy.js | 10 +++ tests/Frontend/e2e/AdminSettingsView.cy.js | 42 ++++++++++++ .../e2e/RoomsViewMembersMemberActions.cy.js | 55 +++++++++++----- tests/Frontend/e2e/RoomsViewSettings.cy.js | 65 ++++++++++++++----- tests/Frontend/fixtures/config.json | 3 +- tests/Frontend/fixtures/settings.json | 1 + 6 files changed, 142 insertions(+), 34 deletions(-) diff --git a/tests/Frontend/e2e/AdminSettingsEdit.cy.js b/tests/Frontend/e2e/AdminSettingsEdit.cy.js index 3c22a5f2df..ec3e689a18 100644 --- a/tests/Frontend/e2e/AdminSettingsEdit.cy.js +++ b/tests/Frontend/e2e/AdminSettingsEdit.cy.js @@ -2279,9 +2279,17 @@ describe("Admin settings with edit permission", function () { cy.get("#password-change-allowed").should("be.checked").click(); }); + cy.get('[data-test="user-search-by-name-field"]') + .should("be.visible") + .and("include.text", "admin.settings.user_search_by_name.title") + .within(() => { + cy.get("#user-search-by-name").should("be.checked").click(); + }); + // Save changes cy.fixture("settings.json").then((settings) => { settings.data.user_password_change_allowed = false; + settings.data.user_search_by_name = false; const saveChangesRequest = interceptIndefinitely( "POST", @@ -2313,6 +2321,7 @@ describe("Admin settings with edit permission", function () { ); expect(formData.get("user_password_change_allowed")).to.equal("0"); + expect(formData.get("user_search_by_name")).to.equal("0"); }); // Check that config is loaded @@ -2324,6 +2333,7 @@ describe("Admin settings with edit permission", function () { // Check that settings are shown correctly cy.get("#password-change-allowed").should("not.be.checked"); + cy.get("#user-search-by-name").should("not.be.checked"); }); it("change recording and statistics settings", function () { diff --git a/tests/Frontend/e2e/AdminSettingsView.cy.js b/tests/Frontend/e2e/AdminSettingsView.cy.js index 32274e4e2f..a5525e57a0 100644 --- a/tests/Frontend/e2e/AdminSettingsView.cy.js +++ b/tests/Frontend/e2e/AdminSettingsView.cy.js @@ -727,6 +727,48 @@ describe("Admin settings with edit permission", function () { .should("be.checked") .and("be.disabled"); }); + + cy.get('[data-test="user-search-by-name-field"]') + .should("be.visible") + .and("include.text", "admin.settings.user_search_by_name.title") + .and("include.text", "admin.settings.user_search_by_name.description") + .within(() => { + cy.get("#user-search-by-name").should("be.checked").and("be.disabled"); + }); + + // Reload with different settings + cy.fixture("settings.json").then((settings) => { + settings.data.user_password_change_allowed = false; + settings.data.user_search_by_name = false; + + cy.intercept("GET", "api/v1/settings", { + statusCode: 200, + body: settings, + }).as("settingsRequest"); + }); + + cy.reload(); + + cy.wait("@settingsRequest"); + + cy.get('[data-test="password-change-allowed-field"]') + .should("be.visible") + .and("include.text", "admin.settings.password_change_allowed") + .within(() => { + cy.get("#password-change-allowed") + .should("not.be.checked") + .and("be.disabled"); + }); + + cy.get('[data-test="user-search-by-name-field"]') + .should("be.visible") + .and("include.text", "admin.settings.user_search_by_name.title") + .and("include.text", "admin.settings.user_search_by_name.description") + .within(() => { + cy.get("#user-search-by-name") + .should("not.be.checked") + .and("be.disabled"); + }); }); it("check recording and statistics settings with only view permission", function () { diff --git a/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js b/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js index 0971e47436..8c48d6f844 100644 --- a/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js +++ b/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js @@ -94,22 +94,6 @@ describe("Rooms view members member actions", function () { cy.get('[data-test="select-user-dropdown"]').find("input").type("aura"); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "La", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "Lau", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "Laur", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { expect(interception.request.query).to.contain({ query: "Laura", @@ -241,6 +225,45 @@ describe("Rooms view members member actions", function () { .should("include.text", "rooms.roles.moderator"); }); + it("add new member with search by name disabled", function () { + cy.fixture("config.json").then((config) => { + config.data.user.search_by_name = false; + + cy.intercept("GET", "api/v1/config", { + statusCode: 200, + body: config, + }); + }); + + cy.visit("/rooms/abc-def-123#tab=members"); + + cy.wait("@roomMembersRequest"); + + cy.get('[data-test="room-members-add-button"]').click(); + + // Click on add single user option + cy.get("#overlay_menu_0") + .should("have.text", "rooms.members.add_single_user") + .click(); + + cy.get('[data-test="room-members-add-single-dialog"]').should("be.visible"); + + // Check prompt + cy.get('[data-test="select-user-dropdown"]').should( + "include.text", + "rooms.members.modals.add.no_options_email_only", + ); + + // Check placeholder and type in input + cy.get('[data-test="select-user-dropdown"]') + .find("input") + .should( + "have.attr", + "placeholder", + "rooms.members.modals.add.placeholder_email_only", + ); + }); + it("add new member errors", function () { cy.visit("/rooms/abc-def-123#tab=members"); cy.wait("@roomRequest"); diff --git a/tests/Frontend/e2e/RoomsViewSettings.cy.js b/tests/Frontend/e2e/RoomsViewSettings.cy.js index 9cf9991a38..feea56093f 100644 --- a/tests/Frontend/e2e/RoomsViewSettings.cy.js +++ b/tests/Frontend/e2e/RoomsViewSettings.cy.js @@ -1926,7 +1926,11 @@ describe("Rooms view settings", function () { // Check placeholder and type in input cy.get('[data-test="new-owner-dropdown"]') .find("input") - .should("have.attr", "placeholder", "app.user_name") + .should( + "have.attr", + "placeholder", + "rooms.members.modals.add.placeholder", + ) .click(); cy.get('[data-test="new-owner-dropdown"]').find("input").type("L"); @@ -1980,22 +1984,6 @@ describe("Rooms view settings", function () { cy.get('[data-test="new-owner-dropdown"]').find("input").type("aura"); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "La", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "Lau", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { - expect(interception.request.query).to.contain({ - query: "Laur", - }); - }); - cy.wait("@userSearchRequest").then((interception) => { expect(interception.request.query).to.contain({ query: "Laura", @@ -2199,6 +2187,49 @@ describe("Rooms view settings", function () { cy.get('[data-test="room-access-code-overlay"]').should("be.visible"); }); + it("transfer ownership with search by name disabled", function () { + cy.fixture("config.json").then((config) => { + config.data.user.search_by_name = false; + + cy.intercept("GET", "api/v1/config", { + statusCode: 200, + body: config, + }); + }); + + cy.visit("/rooms/abc-def-123#tab=settings"); + + cy.wait("@roomSettingsRequest"); + + cy.get("[data-test=room-transfer-ownership-dialog]").should("not.exist"); + cy.get('[data-test="room-transfer-ownership-button"]') + .should("have.text", "rooms.modals.transfer_ownership.title") + .click(); + + // Check that dialog is shown correctly + cy.get("[data-test=room-transfer-ownership-dialog]") + .should("be.visible") + .within(() => { + // Check autofocus + cy.get(".multiselect__content").should("be.visible"); + + // Check prompt + cy.get('[data-test="new-owner-dropdown"]').should( + "include.text", + "rooms.members.modals.add.no_options_email_only", + ); + + // Check placeholder and type in input + cy.get('[data-test="new-owner-dropdown"]') + .find("input") + .should( + "have.attr", + "placeholder", + "rooms.members.modals.add.placeholder_email_only", + ); + }); + }); + it("transfer ownership errors", function () { cy.visit("/rooms/abc-def-123#tab=settings"); diff --git a/tests/Frontend/fixtures/config.json b/tests/Frontend/fixtures/config.json index 74c9bf8782..d3dfe047b4 100644 --- a/tests/Frontend/fixtures/config.json +++ b/tests/Frontend/fixtures/config.json @@ -35,7 +35,8 @@ "room_name_limit": 50 }, "user": { - "password_change_allowed": true + "password_change_allowed": true, + "search_by_name": true }, "banner": {}, "recording": { diff --git a/tests/Frontend/fixtures/settings.json b/tests/Frontend/fixtures/settings.json index 798a5c52a2..e18541d177 100644 --- a/tests/Frontend/fixtures/settings.json +++ b/tests/Frontend/fixtures/settings.json @@ -33,6 +33,7 @@ "room_file_terms_of_use": "Room file terms of use", "room_hide_owner_from_guests": false, "user_password_change_allowed": true, + "user_search_by_name": true, "recording_server_usage_enabled": true, "recording_server_usage_retention_period": -1, "recording_meeting_usage_enabled": true, From 60343a1f6ddd2a382ab89f8aeabb7b6136cb9eb1 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:48:43 +0200 Subject: [PATCH 04/15] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e2f25b6e5..b0353b4664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Environment variable `VERSION` to change the displayed version in the footer ([#3300], [#3302]) - System-wide default welcome message ([#3301]) +- Privacy setting to disable finding users by partial matches of their name or email address ([#2264], [#3316]) ## [v4.17.0] - 2026-07-09 @@ -760,6 +761,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#2165]: https://github.com/THM-Health/PILOS/pull/2165 [#2222]: https://github.com/THM-Health/PILOS/pull/2222 [#2223]: https://github.com/THM-Health/PILOS/pull/2223 +[#2264]: https://github.com/THM-Health/PILOS/issues/2264 [#2265]: https://github.com/THM-Health/PILOS/issues/2265 [#2279]: https://github.com/THM-Health/PILOS/pull/2279 [#2281]: https://github.com/THM-Health/PILOS/pull/2281 @@ -867,6 +869,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#3300]: https://github.com/THM-Health/PILOS/issues/3300 [#3301]: https://github.com/THM-Health/PILOS/pull/3301 [#3302]: https://github.com/THM-Health/PILOS/pull/3302 +[#3316]: https://github.com/THM-Health/PILOS/issues/3316 [unreleased]: https://github.com/THM-Health/PILOS/compare/v4.17.0...develop [v3.0.0]: https://github.com/THM-Health/PILOS/releases/tag/v3.0.0 [v3.0.1]: https://github.com/THM-Health/PILOS/releases/tag/v3.0.1 From fb5fa1c6d8690cdedd970052abe8cd10546973bb Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:06:03 +0200 Subject: [PATCH 05/15] Fix code comments --- tests/Backend/Feature/api/v1/UserTest.php | 2 +- tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js | 2 +- tests/Frontend/e2e/RoomsViewSettings.cy.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Backend/Feature/api/v1/UserTest.php b/tests/Backend/Feature/api/v1/UserTest.php index 19a1f38602..e281efcd60 100644 --- a/tests/Backend/Feature/api/v1/UserTest.php +++ b/tests/Backend/Feature/api/v1/UserTest.php @@ -271,7 +271,7 @@ public function test_search_search_by_name_disabled() $users = []; $users[] = User::factory()->create(['firstname' => 'Gregory', 'lastname' => 'Dumas', 'email' => 'gregory.dumas@example.com']); - // Check with exact lastname match (search for names should not be possible) + // Check with exact firstname match (search for names should not be possible) $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Dumas') ->assertSuccessful() ->assertJsonCount(0, 'data'); diff --git a/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js b/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js index 8c48d6f844..31f3a75e93 100644 --- a/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js +++ b/tests/Frontend/e2e/RoomsViewMembersMemberActions.cy.js @@ -254,7 +254,7 @@ describe("Rooms view members member actions", function () { "rooms.members.modals.add.no_options_email_only", ); - // Check placeholder and type in input + // Check placeholder cy.get('[data-test="select-user-dropdown"]') .find("input") .should( diff --git a/tests/Frontend/e2e/RoomsViewSettings.cy.js b/tests/Frontend/e2e/RoomsViewSettings.cy.js index feea56093f..ed82c41efb 100644 --- a/tests/Frontend/e2e/RoomsViewSettings.cy.js +++ b/tests/Frontend/e2e/RoomsViewSettings.cy.js @@ -2219,7 +2219,7 @@ describe("Rooms view settings", function () { "rooms.members.modals.add.no_options_email_only", ); - // Check placeholder and type in input + // Check placeholder cy.get('[data-test="new-owner-dropdown"]') .find("input") .should( From 0c8da6622e4f8277c203f3176e387e12c6b2c42a Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:06:31 +0200 Subject: [PATCH 06/15] Reset results and show loading during search input --- .../js/components/RoomTabMembersAddSingleModal.vue | 13 ++++++++----- .../js/components/RoomTransferOwnershipButton.vue | 11 ++++++++--- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/resources/js/components/RoomTabMembersAddSingleModal.vue b/resources/js/components/RoomTabMembersAddSingleModal.vue index dc6ef4b1e9..8d75721aa7 100644 --- a/resources/js/components/RoomTabMembersAddSingleModal.vue +++ b/resources/js/components/RoomTabMembersAddSingleModal.vue @@ -189,17 +189,21 @@ defineExpose({ showModal, }); -const onSearchChange = useDebounceFn((query) => { +function onSearchChange(query) { + isLoadingSearch.value = true; + users.value = []; + debouncedFind(query); +} + +const debouncedFind = useDebounceFn((query) => { asyncFind(query); -}, 500); +}, 300); /** * Search for users in database * @param query */ function asyncFind(query) { - isLoadingSearch.value = true; - const config = { params: { query, @@ -210,7 +214,6 @@ function asyncFind(query) { .call("users/search", config) .then((response) => { if (response.status === 204) { - users.value = []; tooManyResults.value = true; return; } diff --git a/resources/js/components/RoomTransferOwnershipButton.vue b/resources/js/components/RoomTransferOwnershipButton.vue index f7bbab9e3c..2c7b2ab5d8 100644 --- a/resources/js/components/RoomTransferOwnershipButton.vue +++ b/resources/js/components/RoomTransferOwnershipButton.vue @@ -279,9 +279,15 @@ function showModal() { modalVisible.value = true; } -const onSearchChange = useDebounceFn((query) => { +function onSearchChange(query) { + isLoadingSearch.value = true; + users.value = []; + debouncedFind(query); +} + +const debouncedFind = useDebounceFn((query) => { asyncFind(query); -}, 500); +}, 300); /** * Search for users in database @@ -300,7 +306,6 @@ function asyncFind(query) { .call("users/search", config) .then((response) => { if (response.status === 204) { - users.value = []; tooManyResults.value = true; return; } From 2715511af817a815c468f2d6ef47c03f9d170717 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 15:06:44 +0200 Subject: [PATCH 07/15] Refactor usage of UserSettings --- app/Http/Controllers/api/v1/UserController.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/api/v1/UserController.php b/app/Http/Controllers/api/v1/UserController.php index 20e7f48d19..9376c668bd 100644 --- a/app/Http/Controllers/api/v1/UserController.php +++ b/app/Http/Controllers/api/v1/UserController.php @@ -47,8 +47,10 @@ public function __construct() * @param UserSearchRequest $request query parameter with search query * @return AnonymousResourceCollection */ - public function search(UserSearchRequest $request, UserSettings $userSettings) + public function search(UserSearchRequest $request) { + $userSettings = app(UserSettings::class); + if (! $request->filled('query')) { abort(204, 'Too many results'); } From 26cacbf37d0c697fd36ea70acb0d244f2e015635 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 17:35:02 +0200 Subject: [PATCH 08/15] Implement review suggestions --- CHANGELOG.md | 2 +- .../Controllers/api/v1/UserController.php | 9 ++++-- tests/Backend/Feature/api/v1/SettingsTest.php | 2 +- tests/Backend/Feature/api/v1/UserTest.php | 29 +++++++++++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efb21e9083..649e02f31d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -875,7 +875,7 @@ You can find the changelog for older versions there [here](https://github.com/TH [#3302]: https://github.com/THM-Health/PILOS/pull/3302 [#3314]: https://github.com/THM-Health/PILOS/issues/3314 [#3315]: https://github.com/THM-Health/PILOS/pull/3315 -[#3316]: https://github.com/THM-Health/PILOS/issues/3316 +[#3316]: https://github.com/THM-Health/PILOS/pull/3316 [unreleased]: https://github.com/THM-Health/PILOS/compare/v4.17.0...develop [v3.0.0]: https://github.com/THM-Health/PILOS/releases/tag/v3.0.0 [v3.0.1]: https://github.com/THM-Health/PILOS/releases/tag/v3.0.1 diff --git a/app/Http/Controllers/api/v1/UserController.php b/app/Http/Controllers/api/v1/UserController.php index 9376c668bd..66d45d3b98 100644 --- a/app/Http/Controllers/api/v1/UserController.php +++ b/app/Http/Controllers/api/v1/UserController.php @@ -32,6 +32,7 @@ use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Password; use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Str; class UserController extends Controller { @@ -42,7 +43,9 @@ public function __construct() } /** - * Search for users in the whole database, based on first name and last name + * Search for users in the whole database + * Depending on the search_by_name setting it will either search of partial matches of firstname, lastname and email + * or only for an exact email match * * @param UserSearchRequest $request query parameter with search query * @return AnonymousResourceCollection @@ -58,7 +61,9 @@ public function search(UserSearchRequest $request) if ($userSettings->search_by_name) { $query = User::withNameOrEmail($request->query('query')); } else { - $query = User::whereLike('email', $request->query('query')); + // Escape wild-card characters and escape backslashes to prevent escaping our added escape backslash + $email = Str::replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $request->query('query')); + $query = User::whereLike('email', $email); } if ($query->count() > config('bigbluebutton.user_search_limit')) { diff --git a/tests/Backend/Feature/api/v1/SettingsTest.php b/tests/Backend/Feature/api/v1/SettingsTest.php index 12454c2951..bf9ded044b 100644 --- a/tests/Backend/Feature/api/v1/SettingsTest.php +++ b/tests/Backend/Feature/api/v1/SettingsTest.php @@ -90,7 +90,7 @@ public function test_view_settings() $this->roomSettings->save(); $this->userSettings->password_change_allowed = true; - $this->userSettings->user_search_by_name = true; + $this->userSettings->search_by_name = true; $this->userSettings->save(); $this->recordingSettings->server_usage_enabled = true; diff --git a/tests/Backend/Feature/api/v1/UserTest.php b/tests/Backend/Feature/api/v1/UserTest.php index e281efcd60..58469961f1 100644 --- a/tests/Backend/Feature/api/v1/UserTest.php +++ b/tests/Backend/Feature/api/v1/UserTest.php @@ -291,6 +291,35 @@ public function test_search_search_by_name_disabled() ->assertSuccessful() ->assertJsonPath('data.0.firstname', $users[0]->firstname) ->assertJsonCount(1, 'data'); + + // Check wildcard characters are treated literally and do not perform pattern matching + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=gregory%') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=gregory_dumas@example.com') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Check if adding a backslash cannot bypass the escaping + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=gregory\%') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Add \ to email and test if it cannot bypass the escaping + $users[0]->email = 'gregory\.dumas@example.com'; + $users[0]->save(); + + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=gregory\%') + ->assertSuccessful() + ->assertJsonCount(0, 'data'); + + // Check if email with all chars can still be found + $users[0]->email = 'gregory\_%.dumas@example.com'; + $users[0]->save(); + + $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=gregory\_%.dumas@example.com') + ->assertSuccessful() + ->assertJsonCount(1, 'data'); } public function test_create() From 6979bc405a52744cc8cd551480c05329ebfda94b Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:24:32 +0200 Subject: [PATCH 09/15] Abort search requests --- .../components/RoomTabMembersAddSingleModal.vue | 17 ++++++++++++++--- .../components/RoomTransferOwnershipButton.vue | 15 ++++++++++++--- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/resources/js/components/RoomTabMembersAddSingleModal.vue b/resources/js/components/RoomTabMembersAddSingleModal.vue index 8d75721aa7..5b21828009 100644 --- a/resources/js/components/RoomTabMembersAddSingleModal.vue +++ b/resources/js/components/RoomTabMembersAddSingleModal.vue @@ -184,6 +184,7 @@ const users = ref([]); const tooManyResults = ref(false); const isLoadingSearch = ref(false); const isLoadingAction = ref(false); +let abortController = new AbortController(); defineExpose({ showModal, @@ -204,15 +205,21 @@ const debouncedFind = useDebounceFn((query) => { * @param query */ function asyncFind(query) { + abortController.abort(); + + abortController = new AbortController(); const config = { params: { query, }, + signal: abortController.signal, }; api .call("users/search", config) .then((response) => { + isLoadingSearch.value = false; + if (response.status === 204) { tooManyResults.value = true; return; @@ -223,10 +230,14 @@ function asyncFind(query) { }) .catch((error) => { tooManyResults.value = false; - api.error(error, { redirectOnUnauthenticated: false }); - }) - .finally(() => { + + if (error.code === "ERR_CANCELED") { + return; + } + isLoadingSearch.value = false; + + api.error(error, { redirectOnUnauthenticated: false }); }); } diff --git a/resources/js/components/RoomTransferOwnershipButton.vue b/resources/js/components/RoomTransferOwnershipButton.vue index 2c7b2ab5d8..9b9fac0b27 100644 --- a/resources/js/components/RoomTransferOwnershipButton.vue +++ b/resources/js/components/RoomTransferOwnershipButton.vue @@ -219,6 +219,7 @@ const modalVisible = ref(false); const users = ref([]); const newOwner = ref(null); const newRoleInRoom = ref(3); +let abortController = new AbortController(); const settingsStore = useSettingsStore(); const formErrors = useFormErrors(); @@ -296,15 +297,20 @@ const debouncedFind = useDebounceFn((query) => { function asyncFind(query) { isLoadingSearch.value = true; + abortController.abort(); + abortController = new AbortController(); const config = { params: { query, }, + signal: abortController.signal, }; api .call("users/search", config) .then((response) => { + isLoadingSearch.value = false; + if (response.status === 204) { tooManyResults.value = true; return; @@ -321,10 +327,13 @@ function asyncFind(query) { }) .catch((error) => { tooManyResults.value = false; - api.error(error, { redirectOnUnauthenticated: false }); - }) - .finally(() => { + + if (error.code === "ERR_CANCELED") { + return; + } + isLoadingSearch.value = false; + api.error(error, { redirectOnUnauthenticated: false }); }); } From 6f85342123b138d203ddd710d86508765930ae73 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 18:25:35 +0200 Subject: [PATCH 10/15] Adjust code comments --- app/Http/Controllers/api/v1/UserController.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Http/Controllers/api/v1/UserController.php b/app/Http/Controllers/api/v1/UserController.php index 66d45d3b98..5380fff572 100644 --- a/app/Http/Controllers/api/v1/UserController.php +++ b/app/Http/Controllers/api/v1/UserController.php @@ -44,7 +44,7 @@ public function __construct() /** * Search for users in the whole database - * Depending on the search_by_name setting it will either search of partial matches of firstname, lastname and email + * Depending on the search_by_name setting it will either search for partial matches of firstname, lastname and email * or only for an exact email match * * @param UserSearchRequest $request query parameter with search query @@ -61,7 +61,7 @@ public function search(UserSearchRequest $request) if ($userSettings->search_by_name) { $query = User::withNameOrEmail($request->query('query')); } else { - // Escape wild-card characters and escape backslashes to prevent escaping our added escape backslash + // Escape wildcard characters and escape backslashes to prevent escaping our added escape backslash $email = Str::replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], $request->query('query')); $query = User::whereLike('email', $email); } From 39a348857d6a19cdac382a6a425ac966f0cd352f Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Tue, 28 Jul 2026 19:28:31 +0200 Subject: [PATCH 11/15] Implement review suggestions --- ...ed.php => 2026_07_15_130614_add_user_search_by_name.php} | 0 tests/Backend/Feature/api/v1/UserTest.php | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) rename database/settings/{2026_07_15_130614_add_user_name_search_disabled.php => 2026_07_15_130614_add_user_search_by_name.php} (100%) diff --git a/database/settings/2026_07_15_130614_add_user_name_search_disabled.php b/database/settings/2026_07_15_130614_add_user_search_by_name.php similarity index 100% rename from database/settings/2026_07_15_130614_add_user_name_search_disabled.php rename to database/settings/2026_07_15_130614_add_user_search_by_name.php diff --git a/tests/Backend/Feature/api/v1/UserTest.php b/tests/Backend/Feature/api/v1/UserTest.php index 58469961f1..40c29d11d4 100644 --- a/tests/Backend/Feature/api/v1/UserTest.php +++ b/tests/Backend/Feature/api/v1/UserTest.php @@ -259,7 +259,7 @@ public function test_search() ->assertJsonCount(1, 'data'); } - public function test_search_search_by_name_disabled() + public function test_search_by_name_disabled() { $searchLimit = 5; config(['bigbluebutton.user_search_limit' => $searchLimit]); @@ -271,12 +271,12 @@ public function test_search_search_by_name_disabled() $users = []; $users[] = User::factory()->create(['firstname' => 'Gregory', 'lastname' => 'Dumas', 'email' => 'gregory.dumas@example.com']); - // Check with exact firstname match (search for names should not be possible) + // Check with exact lastname match (search for names should not be possible) $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Dumas') ->assertSuccessful() ->assertJsonCount(0, 'data'); - // Check with exact lastname match (search for names should not be possible) + // Check with exact firstname match (search for names should not be possible) $this->actingAs($users[0])->getJson(route('api.v1.users.search').'?query=Gregory') ->assertSuccessful() ->assertJsonCount(0, 'data'); From 9f9154aa232205a85524b92f18b9b59c88d12d09 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:56:18 +0200 Subject: [PATCH 12/15] Move user search to a new component --- lang/en/rooms.php | 1 + .../RoomTabMembersAddSingleModal.vue | 114 +------------- .../RoomTransferOwnershipButton.vue | 116 +-------------- resources/js/components/UserSearch.vue | 140 ++++++++++++++++++ 4 files changed, 147 insertions(+), 224 deletions(-) create mode 100644 resources/js/components/UserSearch.vue diff --git a/lang/en/rooms.php b/lang/en/rooms.php index 1aa159746a..6410159561 100644 --- a/lang/en/rooms.php +++ b/lang/en/rooms.php @@ -226,6 +226,7 @@ 'no_result' => 'Oops! No user was found for this query.', 'placeholder' => 'Name or email', 'placeholder_email_only' => 'Email', + 'searching' => 'Searching ...', 'select_role' => 'Please select a role', 'select_user' => 'Please select the user you would like to add', 'too_many_results' => 'Too many users were found. Please enter a more precise search term.', diff --git a/resources/js/components/RoomTabMembersAddSingleModal.vue b/resources/js/components/RoomTabMembersAddSingleModal.vue index 5b21828009..8e3f3115a1 100644 --- a/resources/js/components/RoomTabMembersAddSingleModal.vue +++ b/resources/js/components/RoomTabMembersAddSingleModal.vue @@ -39,59 +39,13 @@ {{ $t("app.user") }} - - - - {{ $t("rooms.members.modals.add.too_many_results") }} - - - {{ $t("rooms.members.modals.add.no_result") }} - - - - {{ - settingsStore.getSetting("user.search_by_name") - ? $t("rooms.members.modals.add.no_options") - : $t("rooms.members.modals.add.no_options_email_only") - }} - - - {{ option.firstname }} {{ option.lastname }}{{ - option.email - }} - - - {{ option.firstname }} {{ option.lastname }} - - + /> @@ -152,13 +106,10 @@ diff --git a/resources/js/components/UserSearch.vue b/resources/js/components/UserSearch.vue new file mode 100644 index 0000000000..aef16a5681 --- /dev/null +++ b/resources/js/components/UserSearch.vue @@ -0,0 +1,140 @@ + + + + + {{ $t("rooms.members.modals.add.too_many_results") }} + + + {{ $t("rooms.members.modals.add.no_result") }} + + + + + {{ $t("rooms.members.modals.add.searching") }} + + + + {{ + settingsStore.getSetting("user.search_by_name") + ? $t("rooms.members.modals.add.no_options") + : $t("rooms.members.modals.add.no_options_email_only") + }} + + + {{ option.firstname }} {{ option.lastname }}{{ + option.email + }} + + + {{ option.firstname }} {{ option.lastname }} + + + + From d852f139a3c7b54636f40c786bee7e73064c5e14 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:17:01 +0200 Subject: [PATCH 13/15] Code cleanup --- resources/js/components/RoomTabMembersAddSingleModal.vue | 2 -- resources/js/components/RoomTransferOwnershipButton.vue | 5 ++--- resources/js/components/UserSearch.vue | 8 ++++++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/resources/js/components/RoomTabMembersAddSingleModal.vue b/resources/js/components/RoomTabMembersAddSingleModal.vue index 8e3f3115a1..ca0deb3ae9 100644 --- a/resources/js/components/RoomTabMembersAddSingleModal.vue +++ b/resources/js/components/RoomTabMembersAddSingleModal.vue @@ -129,7 +129,6 @@ const formErrors = useFormErrors(); const modalVisible = ref(false); const user = ref(null); const role = ref(null); -const users = ref([]); const isLoadingAction = ref(false); defineExpose({ @@ -142,7 +141,6 @@ function showModal() { user.value = null; role.value = null; formErrors.clear(); - users.value = []; modalVisible.value = true; } diff --git a/resources/js/components/RoomTransferOwnershipButton.vue b/resources/js/components/RoomTransferOwnershipButton.vue index 234a7419a3..0c5811d159 100644 --- a/resources/js/components/RoomTransferOwnershipButton.vue +++ b/resources/js/components/RoomTransferOwnershipButton.vue @@ -32,9 +32,10 @@ {{ $t("app.user") }} @@ -163,7 +164,6 @@ const emit = defineEmits(["transferredOwnership"]); const isLoadingAction = ref(false); const modalVisible = ref(false); -const users = ref([]); const newOwner = ref(null); const newRoleInRoom = ref(3); @@ -219,7 +219,6 @@ function transferOwnership() { */ function showModal() { newOwner.value = null; - users.value = []; newRoleInRoom.value = 3; formErrors.clear(); modalVisible.value = true; diff --git a/resources/js/components/UserSearch.vue b/resources/js/components/UserSearch.vue index aef16a5681..cc883178d7 100644 --- a/resources/js/components/UserSearch.vue +++ b/resources/js/components/UserSearch.vue @@ -59,12 +59,12 @@