From c7e8126593db002da9d2c245b5416efc35811015 Mon Sep 17 00:00:00 2001 From: Samuel Weirich <4281791+SamuelWei@users.noreply.github.com> Date: Thu, 13 Aug 2026 16:13:10 +0200 Subject: [PATCH] Refactor personalized links Name is no longer required, allow personalized links where guests can enter their own name if not enforced --- .../api/v1/RoomPersonalizedLinkController.php | 34 +++++------ app/Http/Requests/JoinMeetingRequest.php | 2 +- .../RoomPersonalizedLinkIndexRequest.php | 2 +- .../Requests/RoomPersonalizedLinkRequest.php | 4 +- app/Http/Requests/StartMeetingRequest.php | 2 +- .../RoomPersonalizedLinkResource.php | 4 +- app/Http/Resources/RoomResource.php | 2 +- app/Models/RoomPersonalizedLink.php | 10 ---- app/Services/MeetingService.php | 4 +- ...actor_names_in_room_personalized_links.php | 53 +++++++++++++++++ lang/en/rooms.php | 12 ++-- lang/en/validation.php | 1 + .../components/RoomTabPersonalizedLinks.vue | 17 +++--- .../RoomTabPersonalizedLinksAddButton.vue | 49 +++++++++------- .../RoomTabPersonalizedLinksCopyButton.vue | 12 +--- .../RoomTabPersonalizedLinksDeleteButton.vue | 12 +--- .../RoomTabPersonalizedLinksEditButton.vue | 58 +++++++++++-------- resources/js/views/RoomsView.vue | 9 ++- 18 files changed, 167 insertions(+), 120 deletions(-) create mode 100644 database/migrations/2026_08_13_124758_refactor_names_in_room_personalized_links.php diff --git a/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php b/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php index 4b29b87a83..f5c930b2da 100644 --- a/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php +++ b/app/Http/Controllers/api/v1/RoomPersonalizedLinkController.php @@ -28,11 +28,10 @@ public function index(Room $room, RoomPersonalizedLinkIndexRequest $request) { $additional = []; - // Sort by column, fallback/default is firstname + // Sort by column, fallback/default is description $sortBy = match ($request->query('sort_by')) { - 'lastname' => 'LOWER(lastname)', 'last_usage' => 'last_usage', - default => 'LOWER(firstname)', + default => 'LOWER(description)', }; // Sort direction, fallback/default is asc @@ -62,13 +61,10 @@ public function index(Room $room, RoomPersonalizedLinkIndexRequest $request) // Apply search query if set if ($request->filled('query')) { - // Split search query into single words and search for them in firstname and lastname + // Split search query into single words and search for them in description $searchQueries = explode(' ', preg_replace('/\s\s+/', ' ', $request->query('query'))); foreach ($searchQueries as $searchQuery) { - $resource = $resource->where(function ($query) use ($searchQuery) { - $query->whereLike('firstname', '%'.$searchQuery.'%') - ->orWhereLike('lastname', '%'.$searchQuery.'%'); - }); + $resource = $resource->whereLike('description', '%'.$searchQuery.'%'); } } @@ -87,15 +83,15 @@ public function index(Room $room, RoomPersonalizedLinkIndexRequest $request) */ public function store(Room $room, RoomPersonalizedLinkRequest $request) { - $link = new RoomPersonalizedLink; - $link->firstname = $request->firstname; - $link->lastname = $request->lastname; - $link->role = $request->role; - $room->personalizedLinks()->save($link); + $personalizedLink = new RoomPersonalizedLink; + $personalizedLink->description = $request->description; + $personalizedLink->enforced_name = $request->enforced_name; + $personalizedLink->role = $request->role; + $room->personalizedLinks()->save($personalizedLink); - Log::info('Created new personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $link->role->label(), 'name' => $link->fullname]); + Log::info('Created new personalized room link for {description} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'description' => $personalizedLink->description]); - return new RoomPersonalizedLinkResource($link); + return new RoomPersonalizedLinkResource($personalizedLink); } /** @@ -105,12 +101,12 @@ public function store(Room $room, RoomPersonalizedLinkRequest $request) */ public function update(Room $room, RoomPersonalizedLink $personalizedLink, RoomPersonalizedLinkRequest $request) { - $personalizedLink->firstname = $request->firstname; - $personalizedLink->lastname = $request->lastname; + $personalizedLink->description = $request->description; + $personalizedLink->enforced_name = $request->enforced_name; $personalizedLink->role = $request->role; $personalizedLink->save(); - Log::info('Updated personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'name' => $personalizedLink->fullname]); + Log::info('Updated personalized room link for {description} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'description' => $personalizedLink->description]); return new RoomPersonalizedLinkResource($personalizedLink); } @@ -126,7 +122,7 @@ public function destroy(Room $room, RoomPersonalizedLink $personalizedLink) { $personalizedLink->delete(); - Log::info('Removed personalized room link for guest {name} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'name' => $personalizedLink->fullname]); + Log::info('Removed personalized room link for {description} with the role {role} for room {room}', ['room' => $room->getLogLabel(), 'role' => $personalizedLink->role->label(), 'description' => $personalizedLink->description]); return response()->noContent(); } diff --git a/app/Http/Requests/JoinMeetingRequest.php b/app/Http/Requests/JoinMeetingRequest.php index 44f053cc3a..11299a30a2 100644 --- a/app/Http/Requests/JoinMeetingRequest.php +++ b/app/Http/Requests/JoinMeetingRequest.php @@ -14,7 +14,7 @@ public function rules(): array $personalizedLink = Context::getHidden("room.{$this->room->id}.personalized_link"); $rules = [ - 'name' => auth()->check() || $personalizedLink ? [] : ValidateParticipantNameRequest::participantNameValidationRules(), + 'name' => auth()->check() || $personalizedLink?->enforced_name ? [] : ValidateParticipantNameRequest::participantNameValidationRules(), 'dark_mode' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Requests/RoomPersonalizedLinkIndexRequest.php b/app/Http/Requests/RoomPersonalizedLinkIndexRequest.php index 06b43625a1..4a24c10ca0 100644 --- a/app/Http/Requests/RoomPersonalizedLinkIndexRequest.php +++ b/app/Http/Requests/RoomPersonalizedLinkIndexRequest.php @@ -20,7 +20,7 @@ public function rules(): array return [ 'query' => ['nullable', 'string'], 'filter' => [Rule::in(['participant_role', 'moderator_role'])], - 'sort_by' => [Rule::in(['firstname', 'lastname', 'last_usage'])], + 'sort_by' => [Rule::in(['description', 'last_usage'])], 'sort_direction' => [Rule::in(['asc', 'desc'])], ]; } diff --git a/app/Http/Requests/RoomPersonalizedLinkRequest.php b/app/Http/Requests/RoomPersonalizedLinkRequest.php index b7dd630de6..683fa1c238 100644 --- a/app/Http/Requests/RoomPersonalizedLinkRequest.php +++ b/app/Http/Requests/RoomPersonalizedLinkRequest.php @@ -14,8 +14,8 @@ class RoomPersonalizedLinkRequest extends FormRequest public function rules() { return [ - 'firstname' => ['bail', 'required', 'min:2', 'max:50', new ValidName], - 'lastname' => ['bail', 'required', 'min:2', 'max:50', new ValidName], + 'description' => ['bail', 'required', 'min:2', 'max:50'], + 'enforced_name' => ['bail', 'nullable', 'min:2', 'max:50', new ValidName], 'role' => ['required', Rule::in([RoomUserRole::USER, RoomUserRole::MODERATOR])], ]; } diff --git a/app/Http/Requests/StartMeetingRequest.php b/app/Http/Requests/StartMeetingRequest.php index 8fa084fcab..4107d266a6 100644 --- a/app/Http/Requests/StartMeetingRequest.php +++ b/app/Http/Requests/StartMeetingRequest.php @@ -14,7 +14,7 @@ public function rules(): array $personalizedLink = Context::getHidden("room.{$this->room->id}.personalized_link"); $rules = [ - 'name' => auth()->check() || $personalizedLink ? [] : ValidateParticipantNameRequest::participantNameValidationRules(), + 'name' => auth()->check() || $personalizedLink?->enforced_name ? [] : ValidateParticipantNameRequest::participantNameValidationRules(), 'dark_mode' => ['sometimes', 'boolean'], ]; diff --git a/app/Http/Resources/RoomPersonalizedLinkResource.php b/app/Http/Resources/RoomPersonalizedLinkResource.php index 698e7170d9..4337fdab7f 100644 --- a/app/Http/Resources/RoomPersonalizedLinkResource.php +++ b/app/Http/Resources/RoomPersonalizedLinkResource.php @@ -20,8 +20,8 @@ public function toArray($request) return [ 'id' => $this->id, 'token' => $this->token, - 'firstname' => $this->firstname, - 'lastname' => $this->lastname, + 'description' => $this->description, + 'enforced_name' => $this->enforced_name, 'role' => $this->role, 'expires' => $this->expires, 'last_usage' => $this->last_usage, diff --git a/app/Http/Resources/RoomResource.php b/app/Http/Resources/RoomResource.php index cc784db201..8f1573dfdf 100644 --- a/app/Http/Resources/RoomResource.php +++ b/app/Http/Resources/RoomResource.php @@ -55,7 +55,7 @@ public function getDetails($latestMeeting) } return [ - 'username' => $this->when(! empty($this->personalizedLink), $this->personalizedLink?->fullname), + 'username' => $this->when($this->personalizedLink?->enforced_name !== null, $this->personalizedLink?->enforced_name), 'authenticated' => $this->authenticated, 'legacy_code' => $this->hasLegacyCode, 'description' => $this->when($this->authenticated, $this->description), diff --git a/app/Models/RoomPersonalizedLink.php b/app/Models/RoomPersonalizedLink.php index 4dcd65f06f..c1413bac64 100644 --- a/app/Models/RoomPersonalizedLink.php +++ b/app/Models/RoomPersonalizedLink.php @@ -33,16 +33,6 @@ public function room() return $this->belongsTo(Room::class); } - /** - * Full name of the links owner. - * - * @return string - */ - public function getFullnameAttribute() - { - return $this->firstname.' '.$this->lastname; - } - /** * Expire datetime of the link * diff --git a/app/Services/MeetingService.php b/app/Services/MeetingService.php index 45822b46fd..21d6e64e0d 100644 --- a/app/Services/MeetingService.php +++ b/app/Services/MeetingService.php @@ -642,8 +642,8 @@ public function getJoinUrl(JoinMeetingRequest|StartMeetingRequest $request): str $personalizedLink = Context::getHidden("room.{$this->meeting->room->id}.personalized_link"); if (Auth::guest()) { - if ($personalizedLink) { - $name = $personalizedLink->fullname; + if ($personalizedLink?->enforced_name) { + $name = $personalizedLink->enforced_name; } else { $name = $request->name; } diff --git a/database/migrations/2026_08_13_124758_refactor_names_in_room_personalized_links.php b/database/migrations/2026_08_13_124758_refactor_names_in_room_personalized_links.php new file mode 100644 index 0000000000..4e3a028b19 --- /dev/null +++ b/database/migrations/2026_08_13_124758_refactor_names_in_room_personalized_links.php @@ -0,0 +1,53 @@ +string('description')->after('room_id'); + $table->string('enforced_name')->after('description')->nullable(); + }); + + DB::table('room_personalized_links')->update([ + 'enforced_name' => DB::raw('CONCAT(firstname, " ", lastname)'), + 'description' => DB::raw('CONCAT(firstname, " ", lastname)'), + ]); + + Schema::table('room_personalized_links', function (Blueprint $table) { + $table->dropColumn(['firstname', 'lastname']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('room_personalized_links', function (Blueprint $table) { + Schema::table('room_personalized_links', function (Blueprint $table) { + $table->string('firstname')->after('room_id'); + $table->string('lastname')->after('firstname'); + }); + + DB::table('room_personalized_links')->update([ + 'firstname' => DB::raw('enforced_name'), + 'lastname' => '', + ]); + + Schema::table('room_personalized_links', function (Blueprint $table) { + $table->dropColumn(['description', 'enforced_name']); + }); + }); + } +}; diff --git a/lang/en/rooms.php b/lang/en/rooms.php index b658415765..7c76ab83e5 100644 --- a/lang/en/rooms.php +++ b/lang/en/rooms.php @@ -320,13 +320,15 @@ 'only_used_by_authenticated_users' => 'This room can only be used by authenticated users.', 'personalized_links' => [ 'add' => 'Add personalized room link', - 'confirm_delete' => 'Do you really want to delete the personalized room link for :firstname :lastname?', + 'confirm_delete' => 'Do you really want to delete the personalized room link for :description ?', 'copy' => 'Copy personalized room link to clipboard', - 'copy_aria' => 'Copy personalized room link for :firstname :lastname to clipboard', + 'copy_aria' => 'Copy personalized room link for :description to clipboard', 'delete' => 'Delete personalized room link', - 'delete_aria' => 'Delete personalized room link for :firstname :lastname', + 'delete_aria' => 'Delete personalized room link for :description', 'edit' => 'Edit personalized room link', - 'edit_aria' => 'Edit personalized room link for :firstname :lastname', + 'edit_aria' => 'Edit personalized room link for :description', + 'enforced_name' => '(Optional) Name in video conference', + 'enforced_name_hint' => 'If provided, the user cannot change the name. If left blank, the user is asked to enter a name.', 'expires' => 'Expiry date', 'expires_at' => 'Expires at :date', 'filter' => [ @@ -340,7 +342,7 @@ 'last_used_never' => 'Never used', 'nodata' => 'No personalized room links available!', 'reload_aria' => 'Reload personalized room links', - 'room_link_copied' => 'The personalized room link for :firstname :lastname was copied to your clipboard.', + 'room_link_copied' => 'The personalized room link for :description was copied to your clipboard.', 'search_aria' => 'Search personalized room links', 'sort_ascending' => 'Sort personalized room links ascending', 'sort_by' => 'Sort personalized room links by', diff --git a/lang/en/validation.php b/lang/en/validation.php index ead0a02820..249b77a048 100644 --- a/lang/en/validation.php +++ b/lang/en/validation.php @@ -65,6 +65,7 @@ 'download' => 'Downloadable', 'duration' => 'Max. duration', 'email' => 'Email', + 'enforced_name' => 'Name in video conference', 'everyone_can_start' => 'Everyone can start the meeting', 'excerpt' => 'Excerpt', 'expert_mode' => 'Expert mode', diff --git a/resources/js/components/RoomTabPersonalizedLinks.vue b/resources/js/components/RoomTabPersonalizedLinks.vue index 6b663ae9b4..f5ff4700f4 100644 --- a/resources/js/components/RoomTabPersonalizedLinks.vue +++ b/resources/js/components/RoomTabPersonalizedLinks.vue @@ -163,7 +163,7 @@ >

- {{ item.firstname }} {{ item.lastname }} + {{ item.description }}

@@ -209,8 +209,7 @@ @@ -218,8 +217,8 @@ v-if="userPermissions.can('manageSettings', props.room)" :id="item.id" :room-id="props.room.id" - :firstname="item.firstname" - :lastname="item.lastname" + :description="item.description" + :enforced-name="item.enforced_name" :role="item.role" :disabled="isBusy" @edited="loadData()" @@ -230,8 +229,7 @@ v-if="userPermissions.can('manageSettings', props.room)" :id="item.id" :room-id="props.room.id" - :firstname="item.firstname" - :lastname="item.lastname" + :description="item.description" :disabled="isBusy" @deleted="loadData()" @not-found="loadData()" @@ -268,14 +266,13 @@ const { t } = useI18n(); const personalizedLinks = ref([]); const isBusy = ref(false); const loadingError = ref(false); -const sortField = ref("lastname"); +const sortField = ref("description"); const sortOrder = ref(1); const search = ref(""); const filter = ref("all"); const sortFields = computed(() => [ - { name: t("app.firstname"), value: "firstname" }, - { name: t("app.lastname"), value: "lastname" }, + { name: t("app.description"), value: "description" }, { name: t("rooms.personalized_links.last_usage"), value: "last_usage" }, ]); diff --git a/resources/js/components/RoomTabPersonalizedLinksAddButton.vue b/resources/js/components/RoomTabPersonalizedLinksAddButton.vue index 06c3570694..51a82b6d14 100644 --- a/resources/js/components/RoomTabPersonalizedLinksAddButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksAddButton.vue @@ -46,29 +46,38 @@ :disabled="isLoadingAction" @submit="save" > - -
- + +
+ - +
- -
- + +
+ - + {{ + $t("rooms.personalized_links.enforced_name_hint") + }} +
@@ -136,8 +145,8 @@ const api = useApi(); const formErrors = useFormErrors(); const modalVisible = ref(false); -const firstname = ref(null); -const lastname = ref(null); +const description = ref(null); +const enforced_name = ref(null); const role = ref(null); const isLoadingAction = ref(false); @@ -145,8 +154,8 @@ const isLoadingAction = ref(false); * show modal */ function showModal() { - firstname.value = null; - lastname.value = null; + description.value = null; + enforced_name.value = null; role.value = null; formErrors.clear(); modalVisible.value = true; @@ -162,8 +171,8 @@ function save() { const config = { method: "post", data: { - firstname: firstname.value, - lastname: lastname.value, + description: description.value, + enforced_name: enforced_name.value, role: role.value, }, }; diff --git a/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue b/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue index 1c960bf0a9..63787be33d 100644 --- a/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksCopyButton.vue @@ -6,8 +6,7 @@ icon="fa-solid fa-link" :aria-label=" $t('rooms.personalized_links.copy_aria', { - firstname: props.firstname, - lastname: props.lastname, + description: props.description, }) " data-test="room-personalized-links-copy-button" @@ -30,11 +29,7 @@ const props = defineProps({ type: String, required: true, }, - firstname: { - type: String, - required: true, - }, - lastname: { + description: { type: String, required: true, }, @@ -63,8 +58,7 @@ function copyLink() { navigator.clipboard.writeText(link); toast.info( t("rooms.personalized_links.room_link_copied", { - firstname: props.firstname, - lastname: props.lastname, + description: props.description, }), ); } diff --git a/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue b/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue index b511b9b9d8..dd7701a377 100644 --- a/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksDeleteButton.vue @@ -7,8 +7,7 @@ icon="fa-solid fa-trash" :aria-label=" $t('rooms.personalized_links.delete_aria', { - firstname: props.firstname, - lastname: props.lastname, + description: props.description, }) " data-test="room-personalized-links-delete-button" @@ -50,8 +49,7 @@ {{ $t("rooms.personalized_links.confirm_delete", { - firstname: props.firstname, - lastname: props.lastname, + description: props.description, }) }} @@ -75,11 +73,7 @@ const props = defineProps({ type: Number, required: true, }, - firstname: { - type: String, - required: true, - }, - lastname: { + description: { type: String, required: true, }, diff --git a/resources/js/components/RoomTabPersonalizedLinksEditButton.vue b/resources/js/components/RoomTabPersonalizedLinksEditButton.vue index 0d4e026386..ba0be80650 100644 --- a/resources/js/components/RoomTabPersonalizedLinksEditButton.vue +++ b/resources/js/components/RoomTabPersonalizedLinksEditButton.vue @@ -7,8 +7,7 @@ icon="fa-solid fa-edit" :aria-label=" $t('rooms.personalized_links.edit_aria', { - firstname: props.firstname, - lastname: props.lastname, + description: props.description, }) " data-test="room-personalized-links-edit-button" @@ -52,29 +51,38 @@ :disabled="isLoadingAction" @submit="save" > - -
- + +
+ - +
- -
- + +
+ - + {{ + $t("rooms.personalized_links.enforced_name_hint") + }} +
@@ -140,12 +148,12 @@ const props = defineProps({ type: Number, required: true, }, - firstname: { + description: { type: String, required: true, }, - lastname: { - type: String, + enforcedName: { + type: [String, null], required: true, }, role: { @@ -166,8 +174,8 @@ const toast = useToast(); const { t } = useI18n(); const modalVisible = ref(false); -const newFirstname = ref(null); -const newLastname = ref(null); +const newDescription = ref(null); +const newEnforcedName = ref(null); const newRole = ref(null); const isLoadingAction = ref(false); @@ -175,8 +183,8 @@ const isLoadingAction = ref(false); * show modal */ function showModal() { - newFirstname.value = props.firstname; - newLastname.value = props.lastname; + newDescription.value = props.description; + newEnforcedName.value = props.enforcedName; newRole.value = props.role; formErrors.clear(); modalVisible.value = true; @@ -192,8 +200,8 @@ function save() { const config = { method: "put", data: { - firstname: newFirstname.value, - lastname: newLastname.value, + description: newDescription.value, + enforced_name: newEnforcedName.value, role: newRole.value, }, }; diff --git a/resources/js/views/RoomsView.vue b/resources/js/views/RoomsView.vue index e6b2472cfd..1becb4d836 100644 --- a/resources/js/views/RoomsView.vue +++ b/resources/js/views/RoomsView.vue @@ -857,15 +857,18 @@ const hasValidGuestName = computed(() => { return guestName.value !== "" && !rememberedGuestNameInvalid.value; }); -const hasPersonalizedLinkAuthToken = computed(() => { - return roomAuthToken.value?.type === ROOM_AUTH_TOKEN_TYPE_PERSONALIZED_LINK; +const hasEnforcedName = computed(() => { + return ( + roomAuthToken.value?.type === ROOM_AUTH_TOKEN_TYPE_PERSONALIZED_LINK && + room.value.username !== undefined + ); }); const showAccessCodeOverlay = computed(() => { return ( !room.value.authenticated || (!authStore.isAuthenticated && - !hasPersonalizedLinkAuthToken.value && + !hasEnforcedName.value && !hasValidGuestName.value) ); });