Skip to content

Comments

Add ability to share rooms with roles (groups)#2799

Open
hassanalitamam wants to merge 2 commits intoTHM-Health:developfrom
hassanalitamam:feature/share-room-with-role
Open

Add ability to share rooms with roles (groups)#2799
hassanalitamam wants to merge 2 commits intoTHM-Health:developfrom
hassanalitamam:feature/share-room-with-role

Conversation

@hassanalitamam
Copy link
Contributor

@hassanalitamam hassanalitamam commented Feb 7, 2026

Summary

Implements Feature Request #2190 — allows room owners to share rooms with entire roles (groups) instead of only individual users.

  • New room_role pivot table following the existing room_user pattern
  • Backend API with full CRUD for role-based room membership (add/edit/remove roles)
  • Room model updatesisMember(), isModerator(), isCoOwner(), getRole() now check both individual and role-based membership, with highest role winning
  • Room listing — "Shared rooms" filter includes rooms shared via roles
  • Frontend — New role members section in the Members tab with search, filter, and pagination
  • Translations — en, de, de-gender, fr, fa

API Endpoints

Method Route Description
GET rooms/{room}/role-member List role members
POST rooms/{room}/role-member Add role to room
PUT rooms/{room}/role-member/{role} Update role permission
DELETE rooms/{room}/role-member/{role} Remove role from room

Precedence

When a user has both individual and role-based membership, the highest permission level wins.

Closes #2190

Test plan

  • Run php artisan migrate — verify room_role table is created
  • Add a role to a room — verify users in that role see the room in "Shared rooms"
  • Test permission levels: participant, moderator, co-owner via role
  • Test precedence: user with individual CO_OWNER + role-based USER still shows as CO_OWNER
  • Test removing a role — verify users lose access
  • Run php artisan test — all existing tests pass
  • Run npm run build — frontend builds without errors

Summary by CodeRabbit

  • New Features

    • Assign roles as room members with permission levels (participant, moderator, co-owner)
    • New UI to list, search, filter, sort, add, edit and remove role-members (modals and action buttons)
    • New API endpoints to manage room role memberships
  • Behavior Changes

    • Role-based membership is now included in room membership queries and permission checks
  • Documentation / Localization

    • Added translation strings for role-member UI and validation messages

Implement role-based room membership allowing room owners to share
rooms with entire roles instead of only individual users. All users
belonging to a shared role automatically gain access at the specified
permission level (participant, moderator, or co-owner).

- Add room_role pivot table and RoomRole pivot model
- Update Room model with role-based membership checks
- Add RoomRoleMemberController with CRUD API endpoints
- Update room listing filter to include role-shared rooms
- Add frontend components for role member management
- Add translations for en, de, de-gender, fr, fa
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 7, 2026

Walkthrough

Adds role-based room membership: DB pivot, models, API endpoints and validation, Room membership logic updates, new Vue components for managing role members, and localization strings across locales.

Changes

Cohort / File(s) Summary
Database & Pivot Model
database/migrations/2026_02_07_000001_create_room_role_table.php, app/Models/RoomRole.php
Create room_role pivot table and add RoomRole pivot model with enum casting for role.
Models & Membership Logic
app/Models/Role.php, app/Models/Room.php, app/Http/Middleware/RoomAuthenticate.php
Add Role::rooms() relationship; add roleMembers(), role-derived membership helpers (getRoleMembershipRole, isRoleMember) and integrate role-based checks into Room membership/permission methods; middleware updated to allow role-based access.
API Controllers & Requests
app/Http/Controllers/api/v1/RoomRoleMemberController.php, app/Http/Controllers/api/v1/RoomController.php, app/Http/Requests/AddRoomRoleMember.php, app/Http/Requests/UpdateRoomRoleMember.php
New RoomRoleMemberController with index/store/update/destroy; RoomController@index extended to include role-based room filtering; add form requests for add/update validation (unique role-per-room, enum checks).
API Routes & Resources
routes/api.php, app/Http/Resources/RoomRoleMember.php, app/Http/Resources/Room.php
Register REST routes for room role-members, add RoomRoleMember API resource, and include is_role_member in Room resource payload.
Frontend — Components
resources/js/components/RoomTabMembers.vue, resources/js/components/RoomTabRoleMembers.vue, resources/js/components/RoomTabRoleMembersAddModal.vue, resources/js/components/RoomTabRoleMembersEditButton.vue, resources/js/components/RoomTabRoleMembersDeleteButton.vue
Add role-members UI: new section in members tab and full set of components for listing, searching, filtering, pagination, add/edit/delete flows, and API integration (handles 410/422 responses).
Localization
lang/*/app.php, lang/*/rooms.php, lang/*/validation.php (de, de-gender, en, fa, fr)
Add role_not_member_of_room error key and role_already_member validation message; add members.role_members translation subtree (add/edit/modals/nodata/title/user_count) across locales.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

frontend, UI

Suggested reviewers

  • SamuelWei
  • Sabr1n4W
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Add ability to share rooms with roles (groups)' directly and clearly summarizes the main change—enabling room sharing with roles as a mechanism for group-based access control.
Description check ✅ Passed The description covers all required sections: summary, API endpoints, precedence rules, and a comprehensive test plan. It references the closed issue (#2190) and follows the repository template structure.
Linked Issues check ✅ Passed The PR implements the core coding requirements of issue #2190: adds room_role pivot table, creates API CRUD endpoints, updates Room model membership checks to include role-based logic, adds frontend UI, and supports role-based room visibility in filters.
Out of Scope Changes check ✅ Passed All changes are directly related to the feature: new models, controllers, migrations, API routes, frontend components for role member management, and translations. No unrelated modifications were introduced.
Docstring Coverage ✅ Passed Docstring coverage is 86.36% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@app/Http/Controllers/api/v1/RoomRoleMemberController.php`:
- Around line 72-81: The code calls Role::find($request->role_id) after
attaching and then uses $role->getLogLabel(), which can be null if the role was
deleted concurrently; update the store method to avoid NPE by fetching the Role
safely before using it (e.g., replace Role::find(...) with
Role::findOrFail($request->role_id) or fetch the Role prior to calling
$room->roleMembers()->attach(...)), or guard the log call with null-safe
behavior (use $role?->getLogLabel() or a fallback label) so getLogLabel() is
never invoked on null; adjust the Log::info call accordingly (references: store,
roleMembers()->attach, Role::find, getLogLabel, RoomUserRole::from).
- Around line 39-43: The filter uses wherePivot with fully-qualified column
names causing double-qualification; in RoomRoleMemberController update the
$filter entries to use the bare pivot column name 'role' instead of
'room_role.role' (keep the corresponding RoomUserRole::USER, ::MODERATOR,
::CO_OWNER values) so wherePivot receives just 'role' and not a dotted name;
ensure the rest of the code that applies $filter (the wherePivot call) remains
unchanged and works with the bare column name.

In `@resources/js/components/RoomTabRoleMembers.vue`:
- Around line 69-74: The permission check used in RoomTabRoleMembers.vue is
mismatched: replace calls to userPermissions.can('manageSettings', props.room)
with userPermissions.can('manageMembers', props.room) so the frontend matches
the backend authorization; update all occurrences around the role member UI
(e.g., the RoomTabRoleMembersAddModal v-if ref="addModal" and other buttons
gated by userPermissions.can in this component) to use 'manageMembers' and
ensure the `@added`="loadData()" behavior remains unchanged.

In `@resources/js/components/RoomTabRoleMembersDeleteButton.vue`:
- Around line 99-107: In the catch block for the delete request in
RoomTabRoleMembersDeleteButton.vue, avoid calling api.error() after you've
already handled HTTP 410: detect when error.response.status === env.HTTP_GONE
(the branch that calls emit("gone") and sets modalVisible.value = false) and
return early from the catch handler so api.error(error, {
redirectOnUnauthenticated: false }) is not invoked for that case; update the
catch to short-circuit after the 410 handling so only unhandled errors reach
api.error.

In `@resources/js/components/RoomTabRoleMembersEditButton.vue`:
- Around line 160-172: In the catch handler inside
RoomTabRoleMembersEditButton.vue, after detecting a 410 (env.HTTP_GONE) and
calling emit("gone") and setting modalVisible.value = false, add an early return
to stop execution so the fallback api.error(error, { redirectOnUnauthenticated:
false }) is not invoked for "gone" responses; update the catch flow around the
error.response.status === env.HTTP_GONE branch to return immediately after
handling the gone case.
🧹 Nitpick comments (5)
database/migrations/2026_02_07_000001_create_room_role_table.php (1)

16-22: Consider the naming ambiguity of the role column.

The pivot table room_role links rooms to roles (groups), but the role column stores the permission level (participant/moderator/co-owner). This dual use of "role" — both as the group entity and as the permission level — could be confusing for maintainers. A name like permission or room_user_role would be clearer.

This is a minor naming concern — feel free to defer if the rest of the codebase consistently uses role for permission level in this context (as it does in room_user).

app/Models/Room.php (1)

286-306: Potential N+1 concern: getRoleMembershipRole is called in each of isModerator, isCoOwner, isMember, and getRole.

When rendering the Room resource, all four methods are invoked for the same user, each triggering an independent DB query inside getRoleMembershipRole. Consider caching the result per-user on the model instance (e.g., a simple memoization map) to avoid redundant queries.

♻️ Suggested memoization approach
+    /** `@var` array<int, ?RoomUserRole> */
+    protected array $roleMembershipCache = [];
+
     protected function getRoleMembershipRole(?User $user): ?RoomUserRole
     {
         if ($user === null) {
             return null;
         }
 
+        if (array_key_exists($user->id, $this->roleMembershipCache)) {
+            return $this->roleMembershipCache[$user->id];
+        }
+
         $userRoleIds = $user->roles()->pluck('roles.id');
 
         if ($userRoleIds->isEmpty()) {
+            $this->roleMembershipCache[$user->id] = null;
             return null;
         }
 
         $highestRoleMember = $this->roleMembers()
             ->whereIn('role_id', $userRoleIds)
             ->orderByDesc('room_role.role')
             ->first();
 
-        return $highestRoleMember?->pivot->role;
+        $result = $highestRoleMember?->pivot->role;
+        $this->roleMembershipCache[$user->id] = $result;
+        return $result;
     }
app/Http/Controllers/api/v1/RoomRoleMemberController.php (1)

28-30: The match for sort_by only has a default branch — consider validating or documenting supported sort columns.

Currently any value for sort_by silently falls back to LOWER(name). If more sort options are planned (as the match structure suggests), this is fine scaffolding. Otherwise, a simple hardcoded value would be clearer.

resources/js/components/RoomTabRoleMembersAddModal.vue (2)

145-164: Fetching all roles with per_page: 9999 — consider documenting or capping this.

Loading every role in a single request works for small-to-medium installations, but could become a problem at scale. If the roles API supports returning all items without pagination (e.g., a dedicated unpaginated endpoint or a known sentinel), prefer that. Otherwise, this is an acceptable pragmatic choice — just be aware of the ceiling.


69-109: Duplicate radio-button group across Add and Edit components.

The three-option role radio group (participant / moderator / co-owner) with RoomRoleBadge labels is copy-pasted between RoomTabRoleMembersAddModal.vue (Lines 69-109) and RoomTabRoleMembersEditButton.vue (Lines 52-89). Consider extracting a shared RoomRoleSelector component to reduce duplication.

Also applies to: 52-89

Comment on lines +39 to +43
$filter = match ($request->query('filter')) {
'participant_role' => ['room_role.role', RoomUserRole::USER],
'moderator_role' => ['room_role.role', RoomUserRole::MODERATOR],
'co_owner_role' => ['room_role.role', RoomUserRole::CO_OWNER],
default => null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cat -n app/Http/Controllers/api/v1/RoomRoleMemberController.php | head -100

Repository: THM-Health/PILOS

Length of output: 4225


🏁 Script executed:

git ls-files | grep -i composer

Repository: THM-Health/PILOS

Length of output: 113


🏁 Script executed:

rg -n 'belongsToMany' app/Http/Controllers/api/v1/RoomRoleMemberController.php -B5 -A10

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

rg -n "wherePivot" --type php -B2 -A2

Repository: THM-Health/PILOS

Length of output: 2074


🏁 Script executed:

cat composer.json | grep -A 5 -B 5 "laravel/framework"

Repository: THM-Health/PILOS

Length of output: 408


🏁 Script executed:

# Verify the Room model's relationship definition to confirm the pivot table name
rg -n 'roleMembers' app/Models/Room.php -A5

Repository: THM-Health/PILOS

Length of output: 485


🏁 Script executed:

# Check how wherePivot handles column qualification in Laravel 12.3
# by looking at Laravel's source or related documentation
cat composer.lock | grep -A 10 '"name": "laravel/framework"' | head -20

Repository: THM-Health/PILOS

Length of output: 592


🏁 Script executed:

# Check the database schema to understand the pivot table structure
rg -n 'room_role' database/migrations -t php -A5 -B5 | head -50

Repository: THM-Health/PILOS

Length of output: 1953


🏁 Script executed:

# Look for any other usages of wherePivot with table-prefixed columns
rg -n "wherePivot\(['\"]" --type php

Repository: THM-Health/PILOS

Length of output: 650


🌐 Web query:

Laravel 12 wherePivot column qualification behavior

💡 Result:

In Laravel 12, BelongsToMany::wherePivot() is intended for pivot-table columns and internally uses pivot column qualification (via qualifyPivotColumn) so unqualified column names are scoped to the pivot/intermediate table (i.e., pivot_table.column) to avoid ambiguity in the joined query. It also accepts a Query Expression, which is the escape hatch when you don’t want Laravel to treat/qualify the input as a plain pivot column name. [1], [2]

Practical implications:

  • ->wherePivot('approved', 1) targets the pivot table’s approved column (not the related model’s column) and will be qualified to the pivot table in SQL. [1], [2]
  • If you pass something already “non-simple” (e.g., an Expression), Laravel won’t “string-qualify” it like a normal column name—use this when you need custom SQL / explicit qualification. [2]

Sources: Laravel 12 relationship docs for pivot filtering (wherePivot*) [1] and Laravel 12 API docs showing wherePivot(string|Expression $column, …) plus qualifyPivotColumn(string|Expression $column) [2].

[1] (laravel.com)
[2] (api.laravel.com)

Citations:


🏁 Script executed:

# Check if there are any test files or error logs related to this functionality
find . -name "*test*.php" -type f | xargs grep -l "wherePivot" 2>/dev/null | head -5

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

# Look at the RoomRole model to understand the pivot structure better
cat -n app/Models/RoomRole.php 2>/dev/null | head -50

Repository: THM-Health/PILOS

Length of output: 414


🏁 Script executed:

# Verify the exact Laravel code for wherePivot and qualifyPivotColumn behavior
# by checking if there are any tests that show expected usage
find . -name "*Test*.php" -o -name "*test.php" | xargs grep -l "wherePivot" 2>/dev/null

Repository: THM-Health/PILOS

Length of output: 44


🏁 Script executed:

# Search for any error logs or test database files that might show this bug in action
find . -name "*.log" -o -name "*.sqlite" 2>/dev/null | head -5

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

# Double-check by looking at how the filter is being constructed vs how wherePivot is called elsewhere
echo "=== Filter construction in RoomRoleMemberController ===" && \
sed -n '39,62p' app/Http/Controllers/api/v1/RoomRoleMemberController.php && \
echo -e "\n=== Other wherePivot calls (correct pattern) ===" && \
rg "wherePivot\('role'" app/Models/Room.php -B1 -A1

Repository: THM-Health/PILOS

Length of output: 1501


Bug: wherePivot() will double-qualify the column name—use bare column names only.

wherePivot() internally calls qualifyPivotColumn(), which automatically prepends the pivot table name. Passing 'room_role.role' results in the incorrect qualified name room_role.room_role.role, producing a SQL error. Compare with correct usage elsewhere in the codebase (e.g., Room.php lines 300, 318), which pass just 'role'. The filter keys should be the bare column name.

🐛 Proposed fix
         // Filter by role, fallback/default is no filter
         $filter = match ($request->query('filter')) {
-            'participant_role' => ['room_role.role', RoomUserRole::USER],
-            'moderator_role' => ['room_role.role', RoomUserRole::MODERATOR],
-            'co_owner_role' => ['room_role.role', RoomUserRole::CO_OWNER],
+            'participant_role' => ['role', RoomUserRole::USER],
+            'moderator_role' => ['role', RoomUserRole::MODERATOR],
+            'co_owner_role' => ['role', RoomUserRole::CO_OWNER],
             default => null,
         };
🤖 Prompt for AI Agents
In `@app/Http/Controllers/api/v1/RoomRoleMemberController.php` around lines 39 -
43, The filter uses wherePivot with fully-qualified column names causing
double-qualification; in RoomRoleMemberController update the $filter entries to
use the bare pivot column name 'role' instead of 'room_role.role' (keep the
corresponding RoomUserRole::USER, ::MODERATOR, ::CO_OWNER values) so wherePivot
receives just 'role' and not a dotted name; ensure the rest of the code that
applies $filter (the wherePivot call) remains unchanged and works with the bare
column name.

Comment on lines +72 to +81
public function store(Room $room, AddRoomRoleMember $request)
{
$room->roleMembers()->attach($request->role_id, ['role' => $request->role]);

$role = Role::find($request->role_id);

Log::info('Added role {role} with room role {roomRole} to room {room}', ['room' => $room->getLogLabel(), 'roomRole' => RoomUserRole::from($request->role)->label(), 'role' => $role->getLogLabel()]);

return response()->noContent();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Role::find() can return null in a race condition, causing an NPE on getLogLabel().

If the role is deleted between the attach (Line 74) and find (Line 76), $role is null and $role->getLogLabel() throws a fatal error. Use findOrFail or reuse the role from the relation.

Proposed fix
-        $role = Role::find($request->role_id);
+        $role = Role::findOrFail($request->role_id);
🤖 Prompt for AI Agents
In `@app/Http/Controllers/api/v1/RoomRoleMemberController.php` around lines 72 -
81, The code calls Role::find($request->role_id) after attaching and then uses
$role->getLogLabel(), which can be null if the role was deleted concurrently;
update the store method to avoid NPE by fetching the Role safely before using it
(e.g., replace Role::find(...) with Role::findOrFail($request->role_id) or fetch
the Role prior to calling $room->roleMembers()->attach(...)), or guard the log
call with null-safe behavior (use $role?->getLogLabel() or a fallback label) so
getLogLabel() is never invoked on null; adjust the Log::info call accordingly
(references: store, roleMembers()->attach, Role::find, getLogLabel,
RoomUserRole::from).

Comment on lines +69 to +74
<RoomTabRoleMembersAddModal
v-if="userPermissions.can('manageSettings', props.room)"
ref="addModal"
:room-id="props.room.id"
@added="loadData()"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "manageSettings|manageMembers" --type vue --type js -g '*.vue' -A 2 -B 2 | head -100

Repository: THM-Health/PILOS

Length of output: 87


🏁 Script executed:

rg -n "manageMembers|manageSettings" resources/ -t php -A 3 -B 1 | head -150

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

fd -e php -path "*RoomPolicy*" -o -path "*routes*" | head -20

Repository: THM-Health/PILOS

Length of output: 228


🏁 Script executed:

rg "manageSettings|manageMembers" -A 2 -B 1 | head -200

Repository: THM-Health/PILOS

Length of output: 13707


🏁 Script executed:

fd "RoomPolicy" --type f

Repository: THM-Health/PILOS

Length of output: 123


🏁 Script executed:

fd "routes" --type f | grep -E "php|js" | head -10

Repository: THM-Health/PILOS

Length of output: 98


🏁 Script executed:

find resources/js/components -name "*Room*" -type f | head -20

Repository: THM-Health/PILOS

Length of output: 1000


🏁 Script executed:

cat -n app/Policies/RoomPolicy.php | grep -A 5 -B 2 "manageSettings"

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

rg "manageSettings" app/

Repository: THM-Health/PILOS

Length of output: 42


🏁 Script executed:

cat app/Policies/RoomPolicy.php

Repository: THM-Health/PILOS

Length of output: 6605


🏁 Script executed:

cat resources/js/policies/RoomPolicy.js

Repository: THM-Health/PILOS

Length of output: 2321


Fix permission check mismatch: Use manageMembers instead of manageSettings for role member operations.

The API routes for role member operations (add, edit, delete) use the can:manageMembers,room middleware (routes/api.php), but the UI gates these buttons with userPermissions.can('manageSettings', props.room) (lines 70 and 154). These are different permissions with different names in the authorization policy. While both currently check equivalent conditions (owner, co-owner, or rooms.manage), the mismatch between frontend and backend permission names creates architectural confusion and potential security issues. Change the frontend permission check to match the backend: userPermissions.can('manageMembers', props.room).

🤖 Prompt for AI Agents
In `@resources/js/components/RoomTabRoleMembers.vue` around lines 69 - 74, The
permission check used in RoomTabRoleMembers.vue is mismatched: replace calls to
userPermissions.can('manageSettings', props.room) with
userPermissions.can('manageMembers', props.room) so the frontend matches the
backend authorization; update all occurrences around the role member UI (e.g.,
the RoomTabRoleMembersAddModal v-if ref="addModal" and other buttons gated by
userPermissions.can in this component) to use 'manageMembers' and ensure the
`@added`="loadData()" behavior remains unchanged.

Comment on lines +99 to +107
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

410 errors are handled twice — api.error() still fires after the specific 410 handling.

When the server returns HTTP 410, the code emits gone and closes the modal (lines 101-103), but execution continues to line 106 where api.error() is also called. This likely shows a generic error notification to the user even though the 410 was already handled gracefully.

Proposed fix — return early for handled 410
     .catch((error) => {
       if (error.response) {
         if (error.response.status === env.HTTP_GONE) {
           emit("gone");
           modalVisible.value = false;
+          return;
         }
       }
       api.error(error, { redirectOnUnauthenticated: false });
     })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
return;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
🤖 Prompt for AI Agents
In `@resources/js/components/RoomTabRoleMembersDeleteButton.vue` around lines 99 -
107, In the catch block for the delete request in
RoomTabRoleMembersDeleteButton.vue, avoid calling api.error() after you've
already handled HTTP 410: detect when error.response.status === env.HTTP_GONE
(the branch that calls emit("gone") and sets modalVisible.value = false) and
return early from the catch handler so api.error(error, {
redirectOnUnauthenticated: false }) is not invoked for that case; update the
catch to short-circuit after the 410 handling so only unhandled errors reach
api.error.

Comment on lines +160 to +172
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
}
if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) {
formErrors.set(error.response.data.errors);
return;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing return after handling HTTP 410 — the fallback api.error() fires for "gone" responses too.

When the server returns 410, the code emits "gone" and closes the modal but does not return. Execution falls through to api.error(error, ...) on Line 171, which will display an additional (unwanted) error notification to the user.

🐛 Proposed fix
         if (error.response) {
           if (error.response.status === env.HTTP_GONE) {
             emit("gone");
             modalVisible.value = false;
+            return;
           }
           if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) {
             formErrors.set(error.response.data.errors);
             return;
           }
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
}
if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) {
formErrors.set(error.response.data.errors);
return;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
.catch((error) => {
if (error.response) {
if (error.response.status === env.HTTP_GONE) {
emit("gone");
modalVisible.value = false;
return;
}
if (error.response.status === env.HTTP_UNPROCESSABLE_ENTITY) {
formErrors.set(error.response.data.errors);
return;
}
}
api.error(error, { redirectOnUnauthenticated: false });
})
🤖 Prompt for AI Agents
In `@resources/js/components/RoomTabRoleMembersEditButton.vue` around lines 160 -
172, In the catch handler inside RoomTabRoleMembersEditButton.vue, after
detecting a 410 (env.HTTP_GONE) and calling emit("gone") and setting
modalVisible.value = false, add an early return to stop execution so the
fallback api.error(error, { redirectOnUnauthenticated: false }) is not invoked
for "gone" responses; update the catch flow around the error.response.status ===
env.HTTP_GONE branch to return immediately after handling the gone case.

@samuelwei
Copy link
Collaborator

samuelwei commented Feb 7, 2026

Thank you for your PR!

Here are a few notes regarding this PR

  1. We will not merge this at the moment, as we have the following PR that should be merged first:
  1. We already have several ways the share a room:
  • Via link, internal only without access code
  • Via link, internal only with access code
  • Via link, public without access code
  • Via link, public with access code
  • With a single user
  • With a known guest user via a personalized link

In addition we have to take care of the roles the user get's assigned for each of these options.
Managing this is currently split between the settings and the two tabs "Room members" and "Personalized room links". That makes it hard for the users to get an overview of all the available options to "share" a room.

Instead of just adding a second table on the "Room members" Tab we should instead work a broader concept.

  1. I'm not sure if "Roles" is the correct association. I think a dedicated (new) model "Groups" would be a better fit. Mapping to a group could be the OU in LDAP, subdomain/domain in SSO, etc.

In that context it might make sense to organise groups in a tree. Sharing a room with a higher leaf (group) in the tree shares it with all leafs below.

  1. I think about replacing the direct sharing of a room with an invite system, so users are not added to rooms they don't want to be part of to prevent misuse. Unsure how this would look in a group share scenario. How can we prevent misuse? Do we need special permissions to allow users to share with a group?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Share room with group

2 participants