Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions app/Http/Controllers/api/v1/RoomController.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,21 @@ public function index(ShowRoomsRequest $request)
$query->orWhere('user_id', '=', Auth::user()->id);
}

// rooms where the user is member
// rooms where the user is member (individually or via role)
if ($request->filter_shared) {
// list of room ids where the user is member
// list of room ids where the user is a direct member
$roomMemberships = Auth::user()->sharedRooms->modelKeys();
$query->orWhereIn('rooms.id', $roomMemberships);

// list of room ids where the user is a member via role
$userRoleIds = Auth::user()->roles()->pluck('roles.id');
if ($userRoleIds->isNotEmpty()) {
$query->orWhereIn('rooms.id', function ($subQuery) use ($userRoleIds) {
$subQuery->select('room_id')
->from('room_role')
->whereIn('role_id', $userRoleIds);
});
}
}

// all rooms that are public
Expand Down
116 changes: 116 additions & 0 deletions app/Http/Controllers/api/v1/RoomRoleMemberController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
<?php

namespace App\Http\Controllers\api\v1;

use App\Enums\RoomUserRole;
use App\Http\Controllers\Controller;
use App\Http\Requests\AddRoomRoleMember;
use App\Http\Requests\UpdateRoomRoleMember;
use App\Http\Resources\RoomRoleMember;
use App\Models\Role;
use App\Models\Room;
use App\Settings\GeneralSettings;
use Illuminate\Http\Request;
use Log;

class RoomRoleMemberController extends Controller
{
/**
* Return a list with all role-based members of the room
*
* @return \Illuminate\Http\Resources\Json\AnonymousResourceCollection
*/
public function index(Room $room, Request $request)
{
$additional = [];

// Sort by column, fallback/default is name
$sortBy = match ($request->query('sort_by')) {
default => 'LOWER(name)',
};

// Sort direction, fallback/default is asc
$sortOrder = match ($request->query('sort_direction')) {
'desc' => 'DESC',
default => 'ASC',
};

// 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],
default => null,
};

// Get all role members of the room and sort them
$resource = $room->roleMembers()->withCount('users')->orderByRaw($sortBy.' '.$sortOrder)->orderBy('roles.id');

// count all before applying filters
$additional['meta']['total_no_filter'] = $resource->count();

// Apply search query if set
if ($request->has('query')) {
$resource = $resource->where(function ($query) use ($request) {
$query->whereLike('name', '%'.$request->query('query').'%');
});
}

// Apply filter if set
if ($filter) {
$resource = $resource->wherePivot($filter[0], $filter[1]);
}

return RoomRoleMember::collection($resource->paginate(app(GeneralSettings::class)->pagination_page_size))->additional($additional);
}

/**
* Add a role as a room member
*
* @return \Illuminate\Http\Response
*/
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();
}

/**
* Update a role's room membership role
*
* @return \Illuminate\Http\Response
*/
public function update(Room $room, Role $role, UpdateRoomRoleMember $request)
{
if (! $room->roleMembers()->where('role_id', $role->id)->exists()) {
abort(410, __('app.errors.role_not_member_of_room'));
}
$room->roleMembers()->updateExistingPivot($role, ['role' => $request->role]);

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

return response()->noContent();
}

/**
* Remove a role from room membership
*
* @return \Illuminate\Http\Response
*/
public function destroy(Room $room, Role $role)
{
if (! $room->roleMembers()->where('role_id', $role->id)->exists()) {
abort(410, __('app.errors.role_not_member_of_room'));
}
$room->roleMembers()->detach($role);

Log::info('Removed role {role} from room {room}', ['room' => $room->getLogLabel(), 'role' => $role->getLogLabel()]);

return response()->noContent();
}
}
2 changes: 1 addition & 1 deletion app/Http/Middleware/RoomAuthenticate.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public function handle($request, Closure $next, $allowUnAuthenticated = false)
$token = null;

// requested user is the owner or a member of the room
if (Auth::user() && ($room->owner->is(Auth::user()) || $room->members->contains(Auth::user()) || Auth::user()->can('viewAll', Room::class))) {
if (Auth::user() && ($room->owner->is(Auth::user()) || $room->members->contains(Auth::user()) || $room->roleMembers()->whereIn('role_id', Auth::user()->roles->pluck('id'))->exists() || Auth::user()->can('viewAll', Room::class))) {
$authenticated = true;
}

Expand Down
23 changes: 23 additions & 0 deletions app/Http/Requests/AddRoomRoleMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

namespace App\Http\Requests;

use App\Enums\RoomUserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class AddRoomRoleMember extends FormRequest
{
public function rules()
{
return [
'role_id' => ['required', 'integer', 'exists:App\Models\Role,id',
function ($attribute, $value, $fail) {
if ($this->room->roleMembers()->where('role_id', $value)->exists()) {
$fail(__('validation.custom.room.role_already_member'));
}
}],
'role' => ['required', Rule::in([RoomUserRole::USER, RoomUserRole::MODERATOR, RoomUserRole::CO_OWNER])],
];
}
}
17 changes: 17 additions & 0 deletions app/Http/Requests/UpdateRoomRoleMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace App\Http\Requests;

use App\Enums\RoomUserRole;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;

class UpdateRoomRoleMember extends FormRequest
{
public function rules()
{
return [
'role' => ['required', Rule::in([RoomUserRole::USER, RoomUserRole::MODERATOR, RoomUserRole::CO_OWNER])],
];
}
}
1 change: 1 addition & 0 deletions app/Http/Resources/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public function getDetails($latestMeeting)
'description' => $this->when($this->authenticated, $this->description),
'allow_membership' => $this->getRoomSetting('allow_membership'),
'is_member' => $this->resource->isMember(Auth::user()),
'is_role_member' => $this->resource->isRoleMember(Auth::user()),
'is_moderator' => $this->resource->isModerator(Auth::user(), $this->token),
'is_co_owner' => $this->resource->isCoOwner(Auth::user()),
'can_start' => Gate::inspect('start', [$this->resource, $this->token])->allowed(),
Expand Down
24 changes: 24 additions & 0 deletions app/Http/Resources/RoomRoleMember.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace App\Http\Resources;

use Illuminate\Http\Resources\Json\JsonResource;

class RoomRoleMember extends JsonResource
{
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'role' => $this->pivot->role,
'user_count' => $this->users_count ?? $this->users()->count(),
];
}
}
10 changes: 10 additions & 0 deletions app/Models/Role.php
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public function roomTypes()
return $this->belongsToMany(RoomType::class);
}

/**
* Rooms that are shared with this role.
*
* @return BelongsToMany
*/
public function rooms()
{
return $this->belongsToMany(Room::class, 'room_role')->using(RoomRole::class)->withPivot('role');
}

/**
* Scope a query to only get roles that have a name like the passed one.
*
Expand Down
96 changes: 92 additions & 4 deletions app/Models/Room.php
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,16 @@ public function members()
return $this->belongsToMany(User::class)->using(RoomUser::class)->withPivot('role');
}

/**
* Roles that have membership in the room
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function roleMembers()
{
return $this->belongsToMany(Role::class, 'room_role')->using(RoomRole::class)->withPivot('role');
}

/**
* Meetings
*
Expand Down Expand Up @@ -282,15 +292,35 @@ public function isModerator(?User $user)
return $token->room->is($this) && $token->role == RoomUserRole::MODERATOR;
}

return $user == null ? false : $this->members()->wherePivot('role', RoomUserRole::MODERATOR)->get()->contains($user);
if ($user == null) {
return false;
}

// Check individual membership
if ($this->members()->wherePivot('role', RoomUserRole::MODERATOR)->get()->contains($user)) {
return true;
}

// Check role-based membership
return $this->getRoleMembershipRole($user) === RoomUserRole::MODERATOR;
}

/** Check if user is co owner of this room
* @return bool
*/
public function isCoOwner(?User $user)
{
return $user == null ? false : $this->members()->wherePivot('role', RoomUserRole::CO_OWNER)->get()->contains($user);
if ($user == null) {
return false;
}

// Check individual membership
if ($this->members()->wherePivot('role', RoomUserRole::CO_OWNER)->get()->contains($user)) {
return true;
}

// Check role-based membership
return $this->getRoleMembershipRole($user) === RoomUserRole::CO_OWNER;
}

/**
Expand All @@ -307,7 +337,17 @@ public function isMember(?User $user)
return $token->room->is($this) && ($token->role == RoomUserRole::USER || $token->role == RoomUserRole::MODERATOR);
}

return $user == null ? false : $this->members->contains($user);
if ($user == null) {
return false;
}

// Check individual membership
if ($this->members->contains($user)) {
return true;
}

// Check role-based membership
return $this->getRoleMembershipRole($user) !== null;
}

/**
Expand All @@ -327,14 +367,62 @@ public function getRole(?User $user, ?RoomToken $token): RoomUserRole
return RoomUserRole::OWNER;
}

$individualRole = null;
$member = $this->members()->find($user);
if ($member) {
return $member->pivot->role;
$individualRole = $member->pivot->role;
}

$roleBasedRole = $this->getRoleMembershipRole($user);

// If both exist, take the higher value (higher = more privileges)
if ($individualRole !== null && $roleBasedRole !== null) {
return $individualRole->value >= $roleBasedRole->value ? $individualRole : $roleBasedRole;
}

if ($individualRole !== null) {
return $individualRole;
}

if ($roleBasedRole !== null) {
return $roleBasedRole;
}

return $this->getRoomSetting('default_role');
}

/**
* Get the highest room role granted to the user via role-based membership.
* Returns null if the user has no role-based membership.
*/
protected function getRoleMembershipRole(?User $user): ?RoomUserRole
{
if ($user === null) {
return null;
}

$userRoleIds = $user->roles->pluck('id');

if ($userRoleIds->isEmpty()) {
return null;
}

$highestRoleMember = $this->roleMembers()
->whereIn('role_id', $userRoleIds)
->orderByDesc('room_role.role')
->first();

return $highestRoleMember?->pivot->role;
}

/**
* Check if user is a member of this room via role-based membership only
*/
public function isRoleMember(?User $user): bool
{
return $user !== null && $this->getRoleMembershipRole($user) !== null;
}

/**
* Generate message for moderators inside the meeting
*
Expand Down
15 changes: 15 additions & 0 deletions app/Models/RoomRole.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Models;

use App\Enums\RoomUserRole;
use Illuminate\Database\Eloquent\Relations\Pivot;

class RoomRole extends Pivot
{
protected $table = 'room_role';

protected $casts = [
'role' => RoomUserRole::class,
];
}
Loading