GRN2-253: Added the ability to share rooms across multiple users (#912)

* Added ability to share rooms with other users

* Fixed testcases
v2
Ahmad Farhat 3 years ago committed by farhatahmad
parent 8cbfc3f730
commit 967130e57c
  1. 1
      app/assets/javascripts/application.js
  2. 104
      app/assets/javascripts/room.js
  3. 4
      app/assets/stylesheets/application.scss
  4. 17
      app/assets/stylesheets/rooms.scss
  5. 8
      app/assets/stylesheets/users.scss
  6. 17
      app/controllers/admins_controller.rb
  7. 6
      app/controllers/application_controller.rb
  8. 54
      app/controllers/concerns/populator.rb
  9. 2
      app/controllers/concerns/rolify.rb
  10. 76
      app/controllers/rooms_controller.rb
  11. 8
      app/helpers/admins_helper.rb
  12. 10
      app/models/role.rb
  13. 10
      app/models/room.rb
  14. 2
      app/models/setting.rb
  15. 5
      app/models/shared_access.rb
  16. 17
      app/models/user.rb
  17. 5
      app/views/admins/components/_roles.html.erb
  18. 5
      app/views/admins/components/_server_room_row.html.erb
  19. 23
      app/views/admins/components/_settings.html.erb
  20. 3
      app/views/admins/server_rooms.html.erb
  21. 29
      app/views/rooms/components/_room_block.html.erb
  22. 48
      app/views/rooms/components/_shared_room_block.html.erb
  23. 18
      app/views/rooms/show.html.erb
  24. 2
      app/views/shared/_sessions.html.erb
  25. 39
      app/views/shared/modals/_remove_access_modal.html.erb
  26. 45
      app/views/shared/modals/_share_room_modal.html.erb
  27. 3
      config/application.rb
  28. 21
      config/locales/en.yml
  29. 3
      config/routes.rb
  30. 12
      db/migrate/20191128212935_create_shared_accesses.rb
  31. 11
      db/schema.rb
  32. 3
      lib/assets/_primary_themes.scss
  33. 60
      spec/controllers/admins_controller_spec.rb
  34. 118
      spec/controllers/rooms_controller_spec.rb
  35. 8
      vendor/assets/javascripts/bootstrap-select.min.js
  36. 6
      vendor/assets/stylesheets/bootstrap-select.min.css

@ -34,4 +34,5 @@
//= require jquery-ui/widget
//= require jquery-ui/widgets/sortable
//= require pickr.min.js
//= require bootstrap-select.min.js
//= require_tree .

@ -60,6 +60,69 @@ $(document).on('turbolinks:load', function(){
$(".delete-room").click(function() {
showDeleteRoom(this)
})
$('.selectpicker').selectpicker({
liveSearchPlaceholder: "Start searching..."
});
// Fixes turbolinks issue with bootstrap select
$(window).trigger('load.bs.select.data-api');
$(".share-room").click(function() {
// Update the path of save button
$("#save-access").attr("data-path", $(this).data("path"))
// Get list of users shared with and display them
displaySharedUsers($(this).data("users-path"))
})
$("#shareRoomModal").on("show.bs.modal", function() {
$(".selectpicker").selectpicker('val','')
})
$(".bootstrap-select").on("click", function() {
$(".bs-searchbox").siblings().hide()
})
$(".bs-searchbox input").on("input", function() {
if ($(".bs-searchbox input").val() == '' || $(".bs-searchbox input").val().length < 3) {
$(".bs-searchbox").siblings().hide()
} else {
$(".bs-searchbox").siblings().show()
}
})
$(".remove-share-room").click(function() {
$("#remove-shared-confirm").parent().attr("action", $(this).data("path"))
})
// User selects an option from the Room Access dropdown
$(".bootstrap-select").on("changed.bs.select", function(){
// Get the uid of the selected user
let uid = $(".selectpicker").selectpicker('val')
// If the value was changed to blank, ignore it
if (uid == "") return
let currentListItems = $("#user-list li").toArray().map(user => $(user).data("uid"))
// Check to make sure that the user is not already there
if (!currentListItems.includes(uid)) {
// Create the faded list item and display it
let option = $("option[value='" + uid + "']")
let listItem = document.createElement("li")
listItem.setAttribute('class', 'list-group-item text-left not-saved add-access');
listItem.setAttribute("data-uid", uid)
let spanItem = "<span class='avatar float-left mr-2'>" + option.text().charAt(0) + "</span> <span class='shared-user'>" +
option.text() + " <span class='text-muted'>" + option.data("subtext") + "</span></span>" +
"<span class='text-primary float-right shared-user cursor-pointer' onclick='removeSharedUser(this)'><i class='fas fa-times'></i></span>"
listItem.innerHTML = spanItem
$("#user-list").append(listItem)
}
})
}
});
@ -150,3 +213,44 @@ function ResetAccessCode(){
$("#create-room-access-code").text(getLocalizedString("modal.create_room.access_code_placeholder"))
$("#room_access_code").val(null)
}
function saveAccessChanges() {
let listItemsToAdd = $("#user-list li:not(.remove-shared)").toArray().map(user => $(user).data("uid"))
$.post($("#save-access").data("path"), {add: listItemsToAdd})
}
// Get list of users shared with and display them
function displaySharedUsers(path) {
$.get(path, function(users) {
// Create list element and add to user list
var user_list_html = ""
users.forEach(function(user) {
user_list_html += "<li class='list-group-item text-left' data-uid='" + user.uid + "'>"
if (user.image) {
user_list_html += "<img id='user-image' class='avatar float-left mr-2' src='" + user.image + "'></img>"
} else {
user_list_html += "<span class='avatar float-left mr-2'>" + user.name.charAt(0) + "</span>"
}
user_list_html += "<span class='shared-user'>" + user.name + "<span class='text-muted ml-1'>" + user.uid + "</span></span>"
user_list_html += "<span class='text-primary float-right shared-user cursor-pointer' onclick='removeSharedUser(this)'><i class='fas fa-times'></i></span>"
user_list_html += "</li>"
})
$("#user-list").html(user_list_html)
});
}
// Removes the user from the list of shared users
function removeSharedUser(target) {
let parentLI = target.closest("li")
if (parentLI.classList.contains("not-saved")) {
parentLI.parentNode.removeChild(parentLI)
} else {
parentLI.removeChild(target)
parentLI.classList.add("remove-shared")
}
}

@ -36,14 +36,16 @@
@import "tabler-custom";
@import "font-awesome-sprockets";
@import "font-awesome";
@import "monolith.min.scss";
@import "bootstrap-select.min.css";
@import "utilities/variables";
@import "admins";
@import "main";
@import "rooms";
@import "sessions";
@import "monolith.min.scss";
@import "utilities/fonts";
@import "users";
* {
outline: none !important;

@ -83,3 +83,20 @@
margin-top: -6rem;
font-size: 5rem;
}
.bootstrap-select .dropdown-menu li.active small.text-muted{
color: #9aa0ac !important
}
.not-saved {
color: grey;
background: rgba(0, 40, 100, 0.12);
}
.dropdown-menu.show {
min-height: 0px !important;
}
.remove-shared {
text-decoration: line-through;
}

@ -21,4 +21,12 @@
.user-role-tag{
color: white !important;
}
.shared-user {
line-height: 30px;
}
.bootstrap-select {
border: 1px solid rgba(0, 40, 100, 0.12);
}

@ -22,6 +22,7 @@ class AdminsController < ApplicationController
include Emailer
include Recorder
include Rolify
include Populator
manage_users = [:edit_user, :promote, :demote, :ban_user, :unban_user, :approve, :reset]
manage_deleted_users = [:undelete]
@ -49,11 +50,7 @@ class AdminsController < ApplicationController
# GET /admins/server_recordings
def server_recordings
server_rooms = if Rails.configuration.loadbalanced_configuration
Room.includes(:owner).where(users: { provider: @user_domain }).pluck(:bbb_id)
else
Room.pluck(:bbb_id)
end
server_rooms = rooms_list_for_recordings
@search, @order_column, @order_direction, recs =
all_recordings(server_rooms, params.permit(:search, :column, :direction), true, true)
@ -67,13 +64,9 @@ class AdminsController < ApplicationController
@order_column = params[:column] && params[:direction] != "none" ? params[:column] : "created_at"
@order_direction = params[:direction] && params[:direction] != "none" ? params[:direction] : "DESC"
server_rooms = if Rails.configuration.loadbalanced_configuration
Room.includes(:owner).where(users: { provider: @user_domain })
.admins_search(@search)
.admins_order(@order_column, @order_direction)
else
Room.all.admins_search(@search).admins_order(@order_column, @order_direction)
end
server_rooms = server_rooms_list
@user_list = shared_user_list if shared_access_allowed
@pagy, @rooms = pagy_array(server_rooms)
end

@ -172,6 +172,12 @@ class ApplicationController < ActionController::Base
end
helper_method :configured_providers
# Indicates whether users are allowed to share rooms
def shared_access_allowed
@settings.get_value("Shared Access") == "true"
end
helper_method :shared_access_allowed
# Parses the url for the user domain
def parse_user_domain(hostname)
return hostname.split('.').first if Rails.configuration.url_host.empty?

@ -0,0 +1,54 @@
# frozen_string_literal: true
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
#
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
#
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
module Populator
extend ActiveSupport::Concern
# Returns a list of rooms that are in the same context of the current user
def server_rooms_list
if Rails.configuration.loadbalanced_configuration
Room.includes(:owner).where(users: { provider: @user_domain })
.admins_search(@search)
.admins_order(@order_column, @order_direction)
else
Room.all.admins_search(@search).admins_order(@order_column, @order_direction)
end
end
# Returns list of rooms needed to get the recordings on the server
def rooms_list_for_recordings
if Rails.configuration.loadbalanced_configuration
Room.includes(:owner).where(users: { provider: @user_domain }).pluck(:bbb_id)
else
Room.pluck(:bbb_id)
end
end
# Returns a list of users that are in the same context of the current user
def shared_user_list
roles_can_appear = []
Role.where(provider: @user_domain).each do |role|
roles_can_appear << role.name if role.get_permission("can_appear_in_share_list") && role.name != "super_admin"
end
initial_list = User.where.not(uid: current_user.uid).with_highest_priority_role(roles_can_appear)
return initial_list unless Rails.configuration.loadbalanced_configuration
initial_list.where(provider: @user_domain)
end
end

@ -142,7 +142,7 @@ module Rolify
role_params = params.require(:role).permit(:name)
permission_params = params.require(:role).permit(:can_create_rooms, :send_promoted_email,
:send_demoted_email, :can_edit_site_settings, :can_edit_roles, :can_manage_users,
:can_manage_rooms_recordings, :colour)
:can_manage_rooms_recordings, :can_appear_in_share_list, :colour)
permission_params.transform_values! do |v|
if v == "0"

@ -20,12 +20,15 @@ class RoomsController < ApplicationController
include Pagy::Backend
include Recorder
include Joiner
include Populator
before_action :validate_accepted_terms, unless: -> { !Rails.configuration.terms }
before_action :validate_verified_email, except: [:show, :join],
unless: -> { !Rails.configuration.enable_email_verification }
before_action :find_room, except: [:create, :join_specific_room]
before_action :verify_room_ownership_or_admin, only: [:start, :update_settings, :destroy]
before_action :verify_room_ownership_or_admin_or_shared, only: [:start, :shared_access]
before_action :verify_room_ownership_or_admin, only: [:update_settings, :destroy]
before_action :verify_room_ownership_or_shared, only: [:remove_shared_access]
before_action :verify_room_owner_verified, only: [:show, :join],
unless: -> { !Rails.configuration.enable_email_verification }
before_action :verify_room_owner_valid, only: [:show, :join]
@ -61,14 +64,17 @@ class RoomsController < ApplicationController
def show
@anyone_can_start = JSON.parse(@room[:room_settings])["anyoneCanStart"]
@room_running = room_running?(@room.bbb_id)
@shared_room = room_shared_with_user
# If its the current user's room
if current_user && @room.owned_by?(current_user)
if current_user && (@room.owned_by?(current_user) || @shared_room)
if current_user.highest_priority_role.get_permission("can_create_rooms")
# User is allowed to have rooms
@search, @order_column, @order_direction, recs =
recordings(@room.bbb_id, params.permit(:search, :column, :direction), true)
@user_list = shared_user_list if shared_access_allowed
@pagy, @recordings = pagy_array(recs)
else
# Render view for users that cant create rooms
@ -189,6 +195,55 @@ class RoomsController < ApplicationController
redirect_back fallback_location: room_path(@room)
end
# POST /:room_uid/update_shared_access
def shared_access
begin
current_list = @room.shared_users.pluck(:id)
new_list = User.where(uid: params[:add]).pluck(:id)
# Get the list of users that used to be in the list but were removed
users_to_remove = current_list - new_list
# Get the list of users that are in the new list but not in the current list
users_to_add = new_list - current_list
# Remove users that are removed
SharedAccess.where(room_id: @room.id, user_id: users_to_remove).delete_all unless users_to_remove.empty?
# Add users that are added
users_to_add.each do |id|
SharedAccess.create(room_id: @room.id, user_id: id)
end
flash[:success] = I18n.t("room.shared_access_success")
rescue => e
logger.error "Support: Error in updating room shared access: #{e}"
flash[:alert] = I18n.t("room.shared_access_error")
end
redirect_back fallback_location: room_path
end
# POST /:room_uid/remove_shared_access
def remove_shared_access
begin
SharedAccess.find_by!(room_id: @room.id, user_id: params[:user_id]).destroy
flash[:success] = I18n.t("room.remove_shared_access_success")
rescue => e
logger.error "Support: Error in removing room shared access: #{e}"
flash[:alert] = I18n.t("room.remove_shared_access_error")
end
redirect_to current_user.main_room
end
# GET /:room_uid/shared_users
def shared_users
# Respond with JSON object of users that have access to the room
respond_to do |format|
format.json { render body: @room.shared_users.to_json }
end
end
# GET /:room_uid/logout
def logout
logger.info "Support: #{current_user.present? ? current_user.email : 'Guest'} has left room #{@room.uid}"
@ -229,11 +284,23 @@ class RoomsController < ApplicationController
@room = Room.find_by!(uid: params[:room_uid])
end
# Ensure the user either owns the room or is an admin of the room owner or the room is shared with him
def verify_room_ownership_or_admin_or_shared
return redirect_to root_path unless @room.owned_by?(current_user) ||
room_shared_with_user ||
current_user&.admin_of?(@room.owner)
end
# Ensure the user either owns the room or is an admin of the room owner
def verify_room_ownership_or_admin
return redirect_to root_path if !@room.owned_by?(current_user) && !current_user&.admin_of?(@room.owner)
end
# Ensure the user owns the room or is allowed to start it
def verify_room_ownership_or_shared
return redirect_to root_path unless @room.owned_by?(current_user) || room_shared_with_user
end
def validate_accepted_terms
redirect_to terms_path if current_user && !current_user&.accepted_terms
end
@ -259,6 +326,11 @@ class RoomsController < ApplicationController
@settings.get_value("Room Authentication") == "true" && current_user.nil?
end
# Checks if the room is shared with the user and room sharing is enabled
def room_shared_with_user
shared_access_allowed ? @room.shared_with?(current_user) : false
end
def room_limit_exceeded
limit = @settings.get_value("Room Limit").to_i

@ -37,6 +37,14 @@ module AdminsHelper
end
end
def shared_access_string
if @settings.get_value("Shared Access") == "true"
I18n.t("administrator.site_settings.authentication.enabled")
else
I18n.t("administrator.site_settings.authentication.disabled")
end
end
def recording_default_visibility_string
if @settings.get_value("Default Recording Visibility") == "public"
I18n.t("recording.visibility.public")

@ -42,7 +42,7 @@ class Role < ApplicationRecord
Role.create(name: "super_admin", provider: provider, priority: -2, colour: "#cd201f")
.update_all_role_permissions(can_create_rooms: true,
send_promoted_email: true, send_demoted_email: true, can_edit_site_settings: true,
can_edit_roles: true, can_manage_users: true)
can_edit_roles: true, can_manage_users: true, can_appear_in_share_list: true)
end
def self.create_new_role(role_name, provider)
@ -69,6 +69,7 @@ class Role < ApplicationRecord
update_permission("can_edit_roles", permissions[:can_edit_roles].to_s)
update_permission("can_manage_users", permissions[:can_manage_users].to_s)
update_permission("can_manage_rooms_recordings", permissions[:can_manage_rooms_recordings].to_s)
update_permission("can_appear_in_share_list", permissions[:can_appear_in_share_list].to_s)
end
# Updates the value of the permission and enables it
@ -85,7 +86,12 @@ class Role < ApplicationRecord
value = if permission[:enabled]
permission[:value]
else
"false"
case name
when "can_appear_in_share_list"
Rails.configuration.shared_access_default.to_s
else
"false"
end
end
if return_boolean

@ -26,6 +26,7 @@ class Room < ApplicationRecord
validates :name, presence: true
belongs_to :owner, class_name: 'User', foreign_key: :user_id
has_many :shared_access
def self.admins_search(string)
active_database = Rails.configuration.database_configuration[Rails.env]["adapter"]
@ -59,6 +60,15 @@ class Room < ApplicationRecord
user.rooms.include?(self)
end
def shared_users
User.where(id: shared_access.pluck(:user_id))
end
def shared_with?(user)
return false if user.nil?
shared_users.include?(user)
end
# Determines the invite path for the room.
def invite_path
"#{Rails.configuration.relative_url_root}/#{CGI.escape(uid)}"

@ -43,6 +43,8 @@ class Setting < ApplicationRecord
false
when "Room Limit"
Rails.configuration.number_of_rooms_default
when "Shared Access"
Rails.configuration.shared_access_default
end
end
end

@ -0,0 +1,5 @@
# frozen_string_literal: true
class SharedAccess < ApplicationRecord
belongs_to :room
end

@ -29,6 +29,7 @@ class User < ApplicationRecord
before_destroy :destroy_rooms
has_many :rooms
has_many :shared_access
belongs_to :main_room, class_name: 'Room', foreign_key: :room_id, required: false
has_and_belongs_to_many :roles, -> { includes :role_permissions }, join_table: :users_roles
@ -135,6 +136,11 @@ class User < ApplicationRecord
room_list.where.not(last_session: nil).order("last_session desc") + room_list.where(last_session: nil)
end
# Retrieves a list of rooms that are shared with the user
def shared_rooms
Room.where(id: shared_access.pluck(:room_id))
end
def name_chunk
charset = ("a".."z").to_a - %w(b i l o s) + ("2".."9").to_a - %w(5 8)
chunk = name.parameterize[0...3]
@ -228,11 +234,22 @@ class User < ApplicationRecord
User.where.not(id: with_role(role).pluck(:id))
end
def self.with_highest_priority_role(role)
User.all_users_highest_priority_role.where(roles: { name: role })
end
def self.all_users_with_roles
User.joins("INNER JOIN users_roles ON users_roles.user_id = users.id INNER JOIN roles " \
"ON roles.id = users_roles.role_id INNER JOIN role_permissions ON roles.id = role_permissions.role_id").distinct
end
def self.all_users_highest_priority_role
User.joins("INNER JOIN (SELECT user_id, role_id, min(roles.priority) FROM users_roles " \
"INNER JOIN roles ON users_roles.role_id = roles.id GROUP BY user_id) as a ON " \
"a.user_id = users.id INNER JOIN roles ON roles.id = a.role_id " \
" INNER JOIN role_permissions ON roles.id = role_permissions.role_id").distinct
end
private
def create_reset_activation_digest(token)

@ -73,6 +73,11 @@
<%= f.check_box :can_edit_roles, checked: @selected_role.get_permission("can_edit_roles"), class: "custom-switch-input", disabled: edit_disabled || !current_role.get_permission("can_edit_roles") %>
<span class="custom-switch-indicator float-right"></span>
</label>
<label class="custom-switch pl-0 mt-3 mb-3 w-100 text-left d-inline-block <%="form-disable" if !current_role.get_permission("can_appear_in_share_list") %>">
<span class="ml-0 custom-switch-description"><%= t("administrator.roles.appear_in_share_list")%></span>
<%= f.check_box :can_appear_in_share_list, checked: @selected_role.get_permission("can_appear_in_share_list"), class: "custom-switch-input", disabled: edit_disabled || !current_role.get_permission("can_appear_in_share_list") %>
<span class="custom-switch-indicator float-right"></span>
</label>
<label class="custom-switch pl-0 mt-3 mb-3 w-100 text-left d-inline-block <%="form-disable" if !current_role.get_permission("send_promoted_email") %>">
<span class="ml-0 custom-switch-description"><%= t("administrator.roles.promote_email")%></span>
<%= f.check_box :send_promoted_email, checked: @selected_role.get_permission("send_promoted_email"), class: "custom-switch-input", disabled: edit_disabled || !current_role.get_permission("send_promoted_email") %>

@ -53,6 +53,11 @@
<a href="" data-toggle="modal" data-target="#createRoomModal" class="update-room dropdown-item">
<i class="dropdown-icon fas fa-cog"></i> <%= t("room.settings") %>
</a>
<% if shared_access_allowed %>
<a href="" data-toggle="modal" data-target="#shareRoomModal" class="share-room dropdown-item" data-path="<%= room_shared_access_path(room) %>" data-users-path="<%= room_shared_users_path(room) %>">
<i class="dropdown-icon fas fa-users"></i> <%= t("room.share") %>
</a>
<% end %>
<a href="" data-toggle="modal" data-target="#deleteRoomModal" data-path="<%= room_path(room) %>" data-name="<%= room.name %>" class="delete-room dropdown-item">
<i class="dropdown-icon far fa-trash-alt"></i> <%= t("delete") %>
</a>

@ -99,7 +99,28 @@
</div>
</div>
</div>
<div class="mb-6 row">
<div class="mb-6 row">
<div class="col-12">
<div class="form-group">
<label class="form-label"><%= t("administrator.site_settings.shared_access.title") %></label>
<label class="form-label text-muted"><%= t("administrator.site_settings.shared_access.info") %></label>
<div class="dropdown">
<button class="btn btn-primary dropdown-toggle" type="button" id="room-auth" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<%= shared_access_string %>
</button>
<div class="dropdown-menu" aria-labelledby="room-auth">
<%= button_to admin_update_settings_path(setting: "Shared Access", value: "true"), class: "dropdown-item" do %>
<%= t("administrator.site_settings.authentication.enabled") %>
<% end %>
<%= button_to admin_update_settings_path(setting: "Shared Access", value: "false"), class: "dropdown-item" do %>
<%= t("administrator.site_settings.authentication.disabled") %>
<% end %>
</div>
</div>
</div>
</div>
</div>
<div class="mb-6 row">
<div class="col-12">
<div class="form-group">
<label class="form-label"><%= t("administrator.site_settings.recording_visibility.title") %></label>

@ -28,3 +28,6 @@
<%= render "shared/modals/delete_room_modal" %>
<%= render "shared/modals/create_room_modal" %>
<% if shared_access_allowed %>
<%= render "shared/modals/share_room_modal" %>
<% end %>

@ -46,19 +46,26 @@
</div>
</td>
<td class="text-right">
<div class="item-action dropdown" data-display="static">
<a href="javascript:void(0)" data-toggle="dropdown" data-display="static" class="icon <%= 'invisible' if room == current_user.main_room %>">
<i class="fas fa-ellipsis-v px-4"></i>
</a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-md-left">
<a href="" data-toggle="modal" data-target="#createRoomModal" class="update-room dropdown-item">
<i class="dropdown-icon fas fa-cog"></i> <%= t("room.settings") %>
</a>
<a href="" data-toggle="modal" data-target="#deleteRoomModal" data-path="<%= room_path(room) %>" data-name="<%= room.name %>" class="delete-room dropdown-item">
<i class="dropdown-icon far fa-trash-alt"></i> <%= t("delete") %>
<% unless room == current_user.main_room %>
<div class="item-action dropdown" data-display="static">
<a href="javascript:void(0)" data-toggle="dropdown" data-display="static" class="icon">
<i class="fas fa-ellipsis-v px-4"></i>
</a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-md-left">
<a href="" data-toggle="modal" data-target="#createRoomModal" class="update-room dropdown-item">
<i class="dropdown-icon fas fa-cog"></i> <%= t("room.settings") %>
</a>
<% if shared_access_allowed %>
<a href="" data-toggle="modal" data-target="#shareRoomModal" class="share-room dropdown-item" data-path="<%= room_shared_access_path(room) %>" data-users-path="<%= room_shared_users_path(room) %>">
<i class="dropdown-icon fas fa-users"></i> <%= t("room.share") %>
</a>
<% end %>
<a href="" data-toggle="modal" data-target="#deleteRoomModal" data-path="<%= room_path(room) %>" data-name="<%= room.name %>" class="delete-room dropdown-item">
<i class="dropdown-icon far fa-trash-alt"></i> <%= t("delete") %>
</a>
</div>
</div>
</div>
<% end %>
</td>
</tbody>
</table>

@ -0,0 +1,48 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
%>
<div id="room-block" data-room-uid="<%= room.uid %>" data-room-settings=<%= room.room_settings %> data-room-access-code="<%= room.access_code %>" class="card">
<div class="card-body p-1">
<table class="table table-hover table-vcenter text-wrap table-no-border">
<tbody class="no-border-top">
<td>
<span class="stamp stamp-md bg-primary">
<i class="fas fa-share-alt"></i>
</span>
</td>
<td>
<div id="room-name">
<h4 contenteditable="false" class="m-0 force-text-normal" ><%= room.name %></h4>
</div>
<div class="small text-muted text-break">
<%= t("room.shared_by", email: room.owner.name) %>
</div>
</td>
<td class="text-right">
<div class="item-action dropdown" data-display="static">
<a href="javascript:void(0)" data-toggle="dropdown" data-display="static" class="icon">
<i class="fas fa-ellipsis-v px-4"></i>
</a>
<div class="dropdown-menu dropdown-menu-right dropdown-menu-md-left">
<a href="" data-toggle="modal" data-target="#removeAccessModal" class="remove-share-room dropdown-item" data-path="<%= room_remove_shared_access_path(room) %>">
<i class="dropdown-icon far fa-trash-alt"></i> <%= t("remove") %>
</a>
</div>
</div>
</td>
</tbody>
</table>
</div>
</div>

@ -91,6 +91,17 @@
</div>
<% end %>
<% end %>
<% if shared_access_allowed %>
<% current_user.shared_rooms.each do |room| %>
<div class="col-lg-4 col-md-6 col-sm-12">
<%= link_to room do %>
<%= render "rooms/components/shared_room_block", room: room %>
<% end %>
</div>
<% end %>
<% end %>
<% unless room_limit_exceeded %>
<%= render "rooms/components/create_room_block"%>
<% end %>
@ -98,8 +109,13 @@
</div>
</div>
<%= render "shared/sessions", recordings: @recordings, pagy: @pagy, only_public: false, user_recordings: false, title: t("room.recordings")%>
<%= render "shared/sessions", recordings: @recordings, pagy: @pagy, only_public: false, shared_room: @shared_room, user_recordings: false, title: t("room.recordings")%>
<%= render "shared/modals/delete_room_modal" %>
<%= render "shared/modals/create_room_modal" %>
<% if shared_access_allowed %>
<%= render "shared/modals/share_room_modal" %>
<%= render "shared/modals/remove_access_modal" %>
<% end %>

@ -94,7 +94,7 @@
<% failed_recordings = 0 %>
<% recordings.each do |recording| %>
<% begin %>
<% if only_public %>
<% if only_public || (defined?(shared_room) && shared_room) %>
<%= render "shared/components/public_recording_row", recording: recording %>
<% else %>
<%= render "shared/components/recording_row", recording: recording %>

@ -0,0 +1,39 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
%>
<div class="modal fade" id="removeAccessModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content text-center">
<div class="modal-body">
<div class="card-body p-6">
<div class="card-title">
<h3><%= t("modal.remove_shared.title")%></h3>
</div>
<%= button_to "/", method: :delete, id: "remove-shared-confirm", class: "btn btn-danger my-1 btn-del-room" do %>
<%= hidden_field_tag :user_id, current_user.id %>
<%= t("modal.remove_shared.delete") %>
<% end %>
</div>
<div class="card-footer">
<p id="delete-footer">
<%= t("modal.remove_shared.warning").html_safe %>
</p>
</div>
</div>
</div>
</div>
</div>

@ -0,0 +1,45 @@
<%
# BigBlueButton open source conferencing system - http://www.bigbluebutton.org/.
# Copyright (c) 2018 BigBlueButton Inc. and by respective authors (see below).
# This program is free software; you can redistribute it and/or modify it under the
# terms of the GNU Lesser General Public License as published by the Free Software
# Foundation; either version 3.0 of the License, or (at your option) any later
# version.
#
# BigBlueButton is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
# PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public License along
# with BigBlueButton; if not, see <http://www.gnu.org/licenses/>.
%>
<div class="modal fade" id="shareRoomModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content text-center">
<div class="modal-body">
<div class="card-body p-6">
<div class="card-title">
<h3><%= t("modal.share_access.title") %></h3>
</div>
<select class="selectpicker" title="<%= t("modal.share_access.select") %>..." data-live-search="true" data-virtual-scroll="true" >
<% @user_list.each do |user| %>
<option value="<%= user.uid %>" data-subtext="<%= user.uid %>" ><%= user.name %></option>
<% end %>
</select>
<div class="mt-5 text-left">
<label class="form-label"><%= t("modal.share_access.list") %></label>
<ul id="user-list" class="list-group">
</ul>
</div>
<div class="mt-6">
<button id="save-access" class="btn btn-primary btn-block" onclick="saveAccessChanges()" ><%= t("modal.share_access.save") %></button>
<button class="btn btn-secondary text-primary btn-block" onclick="$('#shareRoomModal').modal('hide')"><%= t("modal.share_access.cancel_changes") %></button>
</div>
</div>
<div class="card-footer">
<p><%= t("modal.share_access.footer") %></p>
</div>
</div>
</div>
</div>
</div>

@ -153,6 +153,9 @@ module Greenlight
# Default limit on number of rooms users can create
config.number_of_rooms_default = 15
# Allow users to share rooms by default
config.shared_access_default = (ENV["SHARED_ACCESS"] == "true").to_s
# Default admin password
config.admin_password_default = ENV['ADMIN_PASSWORD'] || 'administrator'

@ -70,6 +70,9 @@ en:
rooms:
info: Limits the number of rooms that a user can have (including Home Room). This setting does not apply to administrators.
title: Number of Rooms per User
shared_access:
info: Setting to disabled will remove the button from the Room options dropdown, preventing users from sharing rooms
title: Allow Users to Share Rooms
subtitle: Customize Greenlight
title: Site Settings
flash:
@ -342,6 +345,10 @@ en:
with: Sign in with %{provider}
forgot_password: Forgot Password?
rename_recording:
remove_shared:
title: Are you sure you want to remove this room from your room list?
delete: I'm sure, remove this room.
warning: You will <b>not</b> be able to access this room anymore.
room_settings:
title: Room Settings
update: Update Room
@ -353,6 +360,13 @@ en:
footer_text: Adjustment to your room can be done at anytime.
rename_room:
name_placeholder: Enter a new room name...
share_access:
footer: Sharing a room with a user allows them to start the room and view the room's recordings
list: Shared With
title: Share Room Access
save: Save Changes
cancel_changes: Cancel Changes
select: Select User
name_update_success: Room name successfully changed!
no_user_email_exists: There is no existing user with the email specified. Please make sure you typed it correctly.
omniauth_error: An error occured while authenticating with omniauth. Please try again or contact an administrator!
@ -413,6 +427,7 @@ en:
invite:
fail: Your token is either invalid or has expired. If you believe this is a mistake, please contact your administrator.
no_invite: You do not have an invitation to join. Please contact your administrator to receive one.
remove: Remove
rename: Rename
reset_password:
subtitle: Reset Password
@ -460,6 +475,12 @@ en:
room_limit_exceeded: You have exceeded the number of rooms allowed. Please delete %{difference} room(s) to access this room.
sessions: Sessions
settings: Room Settings
share: Manage Access
shared_by: Shared by %{email}
remove_shared_access_success: Successfully removed shared room from your room list
remove_shared_access_error: There was an error removing the shared room from your list
shared_access_success: Room shared successfully
shared_access_error: There was an error sharing the room
start: Start
unavailable: This room is currently unavailable due to the owner's email not being verified.
update_settings_error: There was an error updating the room settings

@ -112,6 +112,9 @@ Rails.application.routes.draw do
post '/', to: 'rooms#join'
patch '/', to: 'rooms#update', as: :update_room
post '/update_settings', to: 'rooms#update_settings'
post '/update_shared_access', to: 'rooms#shared_access', as: :room_shared_access
delete '/remove_shared_access', to: 'rooms#remove_shared_access', as: :room_remove_shared_access
get '/shared_users', to: 'rooms#shared_users', as: :room_shared_users
post '/start', to: 'rooms#start', as: :start_room
get '/logout', to: 'rooms#logout', as: :logout_room
post '/login', to: 'rooms#login', as: :login_room

@ -0,0 +1,12 @@
# frozen_string_literal: true
class CreateSharedAccesses < ActiveRecord::Migration[5.2]
def change
create_table :shared_accesses do |t|
t.belongs_to :room, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
end
end

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_10_23_172511) do
ActiveRecord::Schema.define(version: 2019_11_28_212935) do
create_table "features", force: :cascade do |t|
t.integer "setting_id"
@ -90,6 +90,15 @@ ActiveRecord::Schema.define(version: 2019_10_23_172511) do
t.index ["provider"], name: "index_settings_on_provider"
end
create_table "shared_accesses", force: :cascade do |t|
t.integer "room_id"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["room_id"], name: "index_shared_accesses_on_room_id"
t.index ["user_id"], name: "index_shared_accesses_on_user_id"
end
create_table "users", force: :cascade do |t|
t.integer "room_id"
t.string "provider"

@ -91,7 +91,8 @@ a {
opacity: 0.9;
}
}
&:active {
&:active, &.active {
color: $primary-color !important;
background-color: $primary-color-lighten !important;
}
}

@ -345,34 +345,52 @@ describe AdminsController, type: :controller do
end
end
it "clears all users social uids if clear auth button is clicked" do
allow_any_instance_of(ApplicationController).to receive(:set_user_domain).and_return("provider1")
controller.instance_variable_set(:@user_domain, "provider1")
context "POST #shared_access" do
it "changes the shared access setting" do
allow(Rails.configuration).to receive(:loadbalanced_configuration).and_return(true)
allow_any_instance_of(User).to receive(:greenlight_account?).and_return(true)
@request.session[:user_id] = @admin.id
@request.session[:user_id] = @admin.id
@admin.add_role :super_admin
@admin.update_attribute(:provider, "greenlight")
@user2 = create(:user, provider: "provider1")
@user3 = create(:user, provider: "provider1")
post :update_settings, params: { setting: "Shared Access", value: "false" }
@user.update_attribute(:social_uid, Faker::Internet.password)
@user2.update_attribute(:social_uid, Faker::Internet.password)
@user3.update_attribute(:social_uid, Faker::Internet.password)
feature = Setting.find_by(provider: "provider1").features.find_by(name: "Shared Access")
expect(@user.social_uid).not_to be(nil)
expect(@user2.social_uid).not_to be(nil)
expect(@user3.social_uid).not_to be(nil)
expect(feature[:value]).to eq("false")
expect(response).to redirect_to(admin_site_settings_path)
end
end
post :clear_auth
context "POST #clear_auth" do
it "clears all users social uids if clear auth button is clicked" do
allow_any_instance_of(ApplicationController).to receive(:set_user_domain).and_return("provider1")
controller.instance_variable_set(:@user_domain, "provider1")
@user.reload
@user2.reload
@user3.reload
@request.session[:user_id] = @admin.id
expect(@user.social_uid).to be(nil)
expect(@user2.social_uid).to be(nil)
expect(@user3.social_uid).to be(nil)
@admin.add_role :super_admin
@admin.update_attribute(:provider, "greenlight")
@user2 = create(:user, provider: "provider1")
@user3 = create(:user, provider: "provider1")
@user.update_attribute(:social_uid, Faker::Internet.password)
@user2.update_attribute(:social_uid, Faker::Internet.password)
@user3.update_attribute(:social_uid, Faker::Internet.password)
expect(@user.social_uid).not_to be(nil)
expect(@user2.social_uid).not_to be(nil)
expect(@user3.social_uid).not_to be(nil)
post :clear_auth
@user.reload
@user2.reload
@user3.reload
expect(@user.social_uid).to be(nil)
expect(@user2.social_uid).to be(nil)
expect(@user3.social_uid).to be(nil)
end
end
end

@ -605,4 +605,122 @@ describe RoomsController, type: :controller do
expect(response).to redirect_to room_path(@user1.main_room)
end
end
describe "POST #shared_access" do
before do
@user = create(:user)
@room = create(:room, owner: @user)
@user1 = create(:user)
allow(Rails.configuration).to receive(:shared_access_default).and_return("true")
end
it "shares a room with another user" do
@request.session[:user_id] = @user.id
post :shared_access, params: { room_uid: @room.uid, add: [@user1.uid] }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
expect(flash[:success]).to be_present
expect(response).to redirect_to room_path(@room)
end
it "allows a user to view a shared room and start it" do
@request.session[:user_id] = @user.id
post :shared_access, params: { room_uid: @room.uid, add: [@user1.uid] }
allow(controller).to receive(:current_user).and_return(@user1)
get :show, params: { room_uid: @room.uid }
expect(response).to render_template(:show)
end
it "unshares a room from the user if they are removed from the list" do
SharedAccess.create(room_id: @room.id, user_id: @user1.id)
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
@request.session[:user_id] = @user.id
post :shared_access, params: { room_uid: @room.uid, add: [] }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be false
expect(flash[:success]).to be_present
expect(response).to redirect_to room_path(@room)
end
it "doesn't allow a user to share a room they don't own" do
@request.session[:user_id] = @user1.id
post :shared_access, params: { room_uid: @room.uid, add: [@user1.uid] }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be false
expect(response).to redirect_to root_path
end
it "disables shared room functionality if the site setting is disabled" do
allow_any_instance_of(Setting).to receive(:get_value).and_return("false")
@request.session[:user_id] = @user.id
post :shared_access, params: { room_uid: @room.uid, add: [@user1.uid] }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
allow(controller).to receive(:current_user).and_return(@user1)
get :show, params: { room_uid: @room.uid }
expect(response).to render_template(:join)
end
it "allows admins to update room access" do
@admin = create(:user)
@admin.add_role :admin
@request.session[:user_id] = @admin.id
post :shared_access, params: { room_uid: @room.uid, add: [@user1.uid] }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
expect(flash[:success]).to be_present
expect(response).to redirect_to room_path(@room)
end
it "redirects to root path if not admin of current user" do
allow_any_instance_of(User).to receive(:admin_of?).and_return(false)
@admin = create(:user)
@admin.add_role :admin
@request.session[:user_id] = @admin.id
post :shared_access, params: { room_uid: @room.uid, add: [] }
expect(response).to redirect_to(root_path)
end
end
describe "POST #remove_shared_access" do
before do
@user = create(:user)
@room = create(:room, owner: @user)
@user1 = create(:user)
allow(Rails.configuration).to receive(:shared_access_default).and_return("true")
end
it "unshares a room from the user if they click the remove button" do
SharedAccess.create(room_id: @room.id, user_id: @user1.id)
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
@request.session[:user_id] = @user1.id
post :remove_shared_access, params: { room_uid: @room.uid, user_id: @user1.id }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be false
expect(flash[:success]).to be_present
expect(response).to redirect_to @user1.main_room
end
it "doesn't allow some random user to change share access" do
@user2 = create(:user)
SharedAccess.create(room_id: @room.id, user_id: @user1.id)
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
@request.session[:user_id] = @user2.id
post :remove_shared_access, params: { room_uid: @room.uid, user_id: @user1.id }
expect(SharedAccess.exists?(room_id: @room.id, user_id: @user1.id)).to be true
expect(response).to redirect_to root_path
end
end
end

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long