diff --git a/.gitignore b/.gitignore index 953e12ed..6014a7aa 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,8 @@ env # IDEs .idea .idea/** +.vscode +.vscode/** config/terms.md coverage* diff --git a/.rubocop.yml b/.rubocop.yml index 6c4f976b..eebb25d8 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -41,6 +41,10 @@ Style/MixinUsage: Style/SymbolArray: Enabled: false +# Don't use begin blocks when they are not needed. +Style/RedundantBegin: + Enabled: false + # Use `%`-literal delimiters consistently Style/PercentLiteralDelimiters: Enabled: false diff --git a/app/assets/javascripts/admins.js b/app/assets/javascripts/admins.js index 4d5da355..0dff33ee 100644 --- a/app/assets/javascripts/admins.js +++ b/app/assets/javascripts/admins.js @@ -31,32 +31,58 @@ $(document).on('turbolinks:load', function(){ $("#delete-confirm").parent().attr("action", url) }) - $('.colorinput').ColorPicker({ - onHide: function (colpkr) { - var colour = $("#user-colour").val(); + //clear the role filter if user clicks on the x + $(".clear-role").click(function(data) { + search = new URL(location.href).searchParams.get('search') - // Update the color in the database and reload the page - $.post($("#coloring-path").val(), {color: colour}).done(function(data) { - location.reload() - }); - }, - - onSubmit: function(hsb, hex, rgb, el) { - $.post($("#coloring-path").val(), {color: '#' + hex}).done(function(data) { - location.reload() - }); - }, - + url = window.location.pathname + "?page=1" + + if (search) { + url += "&search=" + search + } + + window.location.replace(url); + }) + + /* COLOR SELECTORS */ + + $('#colorinput-regular').ColorPicker({ onBeforeShow: function () { - var colour = $("#user-colour").val(); + var colour = rgb2hex($("#colorinput-regular").css("background-color")) $(this).ColorPickerSetColor(colour); }, + onSubmit: function(_hsb, hex, _rgb, _el) { + $.post($("#coloring-path-regular").val(), {color: '#' + hex}).done(function(data) { + location.reload() + }); + }, + }); - onChange: function (hsb, hex, rgb) { - $('.colorinput span').css('backgroundColor', '#' + hex); - $("#user-colour").val('#' + hex); - } + $('#colorinput-lighten').ColorPicker({ + onBeforeShow: function () { + var colour = rgb2hex($("#colorinput-lighten").css("background-color")) + + $(this).ColorPickerSetColor(colour); + }, + onSubmit: function(_hsb, hex, _rgb, _el) { + $.post($("#coloring-path-lighten").val(), {color: '#' + hex}).done(function(data) { + location.reload() + }); + }, + }); + + $('#colorinput-darken').ColorPicker({ + onBeforeShow: function () { + var colour = rgb2hex($("#colorinput-darken").css("background-color")) + + $(this).ColorPickerSetColor(colour); + }, + onSubmit: function(_hsb, hex, _rgb, _el) { + $.post($("#coloring-path-darken").val(), {color: '#' + hex}).done(function(data) { + location.reload() + }); + }, }); } @@ -79,3 +105,25 @@ function changeBrandingImage(path) { var url = $("#branding-url").val() $.post(path, {url: url}) } + +// Filters by role +function filterRole(role) { + search = new URL(location.href).searchParams.get('search') + + url = window.location.pathname + "?page=1" + "&role=" + role + + if (search) { + url += "&search=" + search + } + + window.location.replace(url); +} + +function rgb2hex(rgb) { + rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/); + function hex(x) { + return ("0" + parseInt(x).toString(16)).slice(-2); + } + return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]); +} + diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 1fbb9fc5..842af35a 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -17,7 +17,8 @@ $(document).on('turbolinks:load', function(){ // Stores the current url when the user clicks the sign in button $(".sign-in-button").click(function(){ - document.cookie ="return_to=" + window.location.href + var url = [location.protocol, '//', location.host, location.pathname].join(''); + document.cookie ="return_to=" + url }) // Checks to see if the user provided an image url and displays it if they did diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js index 80642256..c7d6d8db 100644 --- a/app/assets/javascripts/search.js +++ b/app/assets/javascripts/search.js @@ -77,10 +77,19 @@ function searchPage() { var controller = $("body").data('controller'); var action = $("body").data('action'); + // Check if the user filtered by role + role = new URL(location.href).searchParams.get('role') + + url = window.location.pathname + "?page=1&search=" + search + + if (role) { + url += "&role=" + role + } + if(controller === "rooms" && action === "show"){ - window.location.replace(window.location.pathname + "?page=1&search=" + search + "#recordings-table"); + window.location.replace(url + "#recordings-table"); } else{ - window.location.replace(window.location.pathname + "?page=1&search=" + search); + window.location.replace(url); } } @@ -90,9 +99,17 @@ function clearSearch() { var controller = $("body").data('controller'); var action = $("body").data('action'); + role = new URL(location.href).searchParams.get('role') + + url = window.location.pathname + "?page=1" + + if (role) { + url += "&role=" + role + } + if(controller === "rooms" && action === "show"){ - window.location.replace(window.location.pathname + "?page=1" + "#recordings-table"); + window.location.replace(url + "#recordings-table"); } else{ - window.location.replace(window.location.pathname + "?page=1"); + window.location.replace(url); } } diff --git a/app/assets/stylesheets/_tabler-custom.scss b/app/assets/stylesheets/_tabler-custom.scss index c87cabeb..9df7c337 100644 --- a/app/assets/stylesheets/_tabler-custom.scss +++ b/app/assets/stylesheets/_tabler-custom.scss @@ -59,7 +59,7 @@ @import "tabler/stamp"; //@import "tabler/chat"; //@import "tabler/example"; -//@import "tabler/tag"; +@import "tabler/tag"; //@import "tabler/syntax"; //@import "tabler/infobox"; //@import "tabler/carousel"; diff --git a/app/assets/stylesheets/admins.scss b/app/assets/stylesheets/admins.scss index 2e87b611..56dbb86f 100644 --- a/app/assets/stylesheets/admins.scss +++ b/app/assets/stylesheets/admins.scss @@ -15,8 +15,8 @@ // with BigBlueButton; if not, see . #users-table { - .user-role:hover { - cursor: default; + .user-role { + color: white !important; } } @@ -31,6 +31,14 @@ } } +.tag i { + color: white !important; +} + #branding-image{ z-index: auto; } + +.authentication-required{ + padding-top: 2px; +} \ No newline at end of file diff --git a/app/assets/stylesheets/utilities/_primary_themes.scss b/app/assets/stylesheets/utilities/_primary_themes.scss index 33d29a56..33d48d31 100644 --- a/app/assets/stylesheets/utilities/_primary_themes.scss +++ b/app/assets/stylesheets/utilities/_primary_themes.scss @@ -1,3 +1,9 @@ +.btn { + &:focus { + box-shadow: 0 0 0 2px $primary-color-lighten; + } +} + .btn-primary, .btn-primary:visited, .btn-primary i { @@ -9,7 +15,6 @@ .btn-primary:active, .btn-primary:active:focus, .btn-primary:active:hover, -.btn-primary:focus, .btn-primary:hover, .btn-primary:hover i { background-color: $primary-color-darken !important; @@ -17,6 +22,19 @@ color: white !important; } +.btn-primary:focus { + background-color: $primary-color-darken !important; + border-color: $primary-color-darken !important; + color: white !important; + box-shadow: 0 0 0 2px $primary-color-lighten !important; +} + +.custom-checkbox .custom-control-input:checked ~ .custom-control-label::before{ + background-color: $primary-color !important; + border-color: $primary-color !important; + color: white !important; +} + a { color: $primary-color !important; } @@ -39,7 +57,7 @@ a { } &:focus { - box-shadow: 0 0 0 2px $primary-color-lighten; + box-shadow: 0 0 0 2px $primary-color-lighten !important; } } @@ -91,6 +109,7 @@ input:focus, select:focus { .bg-primary { background-color: $primary-color !important; + color: white !important; } .btn-danger { @@ -123,3 +142,21 @@ input:focus, select:focus { } } } + +.primary-regular { + background-color: $primary-color !important; + border-color: $primary-color !important; + color: white !important; +} + +.primary-lighten { + background-color: $primary-color-lighten !important; + border-color: $primary-color-lighten !important; + color: $primary-color !important; +} + +.primary-darken { + background-color: $primary-color-darken !important; + border-color: $primary-color-darken !important; + color: white !important; +} \ No newline at end of file diff --git a/app/controllers/account_activations_controller.rb b/app/controllers/account_activations_controller.rb index 80e7ba22..1aa32ca5 100644 --- a/app/controllers/account_activations_controller.rb +++ b/app/controllers/account_activations_controller.rb @@ -32,6 +32,10 @@ class AccountActivationsController < ApplicationController if @user && !@user.activated? && @user.authenticated?(:activation, params[:token]) @user.activate + # Redirect user to root with account pending flash if account is still pending + return redirect_to root_path, + flash: { success: I18n.t("registration.approval.signup") } if @user.has_role?(:pending) + flash[:success] = I18n.t("verify.activated") + " " + I18n.t("verify.signin") redirect_to signin_path else diff --git a/app/controllers/admins_controller.rb b/app/controllers/admins_controller.rb index 7d3fb668..50adb163 100644 --- a/app/controllers/admins_controller.rb +++ b/app/controllers/admins_controller.rb @@ -18,30 +18,30 @@ class AdminsController < ApplicationController include Pagy::Backend + include Themer + include Emailer + + manage_users = [:edit_user, :promote, :demote, :ban_user, :unban_user, :approve] + site_settings = [:branding, :coloring, :coloring_lighten, :coloring_darken, + :registration_method, :room_authentication] + authorize_resource class: false - before_action :find_user, only: [:edit_user, :promote, :demote, :ban_user, :unban_user] - before_action :verify_admin_of_user, only: [:edit_user, :promote, :demote, :ban_user, :unban_user] - before_action :find_setting, only: [:branding, :coloring] + before_action :find_user, only: manage_users + before_action :verify_admin_of_user, only: manage_users + before_action :find_setting, only: site_settings # GET /admins def index @search = params[:search] || "" @order_column = params[:column] && params[:direction] != "none" ? params[:column] : "created_at" @order_direction = params[:direction] && params[:direction] != "none" ? params[:direction] : "DESC" + @role = params[:role] || "" - if Rails.configuration.loadbalanced_configuration - @pagy, @users = pagy(User.without_role(:super_admin) - .where(provider: user_settings_provider) - .where.not(id: current_user.id) - .admins_search(@search) - .admins_order(@order_column, @order_direction)) - else - @pagy, @users = pagy(User.where.not(id: current_user.id) - .admins_search(@search) - .admins_order(@order_column, @order_direction)) - end + @pagy, @users = pagy(user_list) end + # MANAGE USERS + # GET /admins/edit/:user_uid def edit_user render "admins/index", locals: { setting_id: "account" } @@ -50,29 +50,24 @@ class AdminsController < ApplicationController # POST /admins/promote/:user_uid def promote @user.add_role :admin + + send_user_promoted_email(@user) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.promoted") } end # POST /admins/demote/:user_uid def demote @user.remove_role :admin + + send_user_demoted_email(@user) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.demoted") } end - # POST /admins/branding - def branding - @settings.update_value("Branding Image", params[:url]) - redirect_to admins_path - end - - # POST /admins/color - def coloring - @settings.update_value("Primary Color", params[:color]) - redirect_to admins_path(setting: "site_settings") - end - # POST /admins/ban/:user_uid def ban_user + @user.roles = [] @user.add_role :denied redirect_to admins_path, flash: { success: I18n.t("administrator.flash.banned") } end @@ -80,9 +75,84 @@ class AdminsController < ApplicationController # POST /admins/unban/:user_uid def unban_user @user.remove_role :denied + @user.add_role :user redirect_to admins_path, flash: { success: I18n.t("administrator.flash.unbanned") } end + # POST /admins/approve/:user_uid + def approve + @user.remove_role :pending + + send_user_approved_email(@user) + + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.approved") } + end + + # POST /admins/invite + def invite + email = params[:invite_user][:email] + + begin + invitation = create_or_update_invite(email) + + send_invitation_email(current_user.name, email, invitation.invite_token) + rescue => e + logger.error "Error in email delivery: #{e}" + flash[:alert] = I18n.t(params[:message], default: I18n.t("delivery_error")) + else + flash[:success] = I18n.t("administrator.flash.invite", email: email) + end + + redirect_to admins_path + end + + # SITE SETTINGS + + # POST /admins/branding + def branding + @settings.update_value("Branding Image", params[:url]) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.settings.image") } + end + + # POST /admins/color + def coloring + @settings.update_value("Primary Color", params[:color]) + @settings.update_value("Primary Color Lighten", color_lighten(params[:color])) + @settings.update_value("Primary Color Darken", color_darken(params[:color])) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.settings") } + end + + def coloring_lighten + @settings.update_value("Primary Color Lighten", params[:color]) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.settings") } + end + + def coloring_darken + @settings.update_value("Primary Color Darken", params[:color]) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.settings") } + end + + # POST /admins/room_authentication + def room_authentication + @settings.update_value("Room Authentication", params[:value]) + redirect_to admins_path, flash: { success: I18n.t("administrator.flash.settings") } + end + + # POST /admins/registration_method/:method + def registration_method + new_method = Rails.configuration.registration_methods[params[:method].to_sym] + + # Only allow change to Join by Invitation if user has emails enabled + if !Rails.configuration.enable_email_verification && new_method == Rails.configuration.registration_methods[:invite] + redirect_to admins_path, + flash: { alert: I18n.t("administrator.flash.invite_email_verification") } + else + @settings.update_value("Registration Method", new_method) + redirect_to admins_path, + flash: { success: I18n.t("administrator.flash.registration_method_updated") } + end + end + private def find_user @@ -97,4 +167,38 @@ class AdminsController < ApplicationController redirect_to admins_path, flash: { alert: I18n.t("administrator.flash.unauthorized") } unless current_user.admin_of?(@user) end + + # Gets the list of users based on your configuration + def user_list + list = if @role.present? + User.with_role(@role.to_sym).where.not(id: current_user.id) + else + User.where.not(id: current_user.id) + end + + if Rails.configuration.loadbalanced_configuration + list.where(provider: user_settings_provider) + .admins_search(@search) + .admins_order(@order_column, @order_direction) + else + list.admins_search(@search) + .admins_order(@order_column, @order_direction) + end + end + + # Creates the invite if it doesn't exist, or updates the updated_at time if it does + def create_or_update_invite(email) + invite = Invitation.find_by(email: email, provider: @user_domain) + + # Invite already exists + if invite.present? + # Updates updated_at to now + invite.touch + else + # Creates invite + invite = Invitation.create(email: email, provider: @user_domain) + end + + invite + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index dc241760..95bb979b 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,6 +19,7 @@ require 'bigbluebutton_api' class ApplicationController < ActionController::Base + include ApplicationHelper include SessionsHelper include ThemingHelper @@ -26,7 +27,10 @@ class ApplicationController < ActionController::Base before_action :set_locale before_action :check_admin_password before_action :set_user_domain - before_action :check_if_unbanned + before_action :check_user_role + + # Manually handle BigBlueButton errors + rescue_from BigBlueButton::BigBlueButtonException, with: :handle_bigbluebutton_error # Force SSL for loadbalancer configurations. before_action :redirect_to_https @@ -84,7 +88,7 @@ class ApplicationController < ActionController::Base helper_method :recording_thumbnails? def allow_greenlight_users? - Rails.configuration.greenlight_accounts + allow_greenlight_accounts? end helper_method :allow_greenlight_users? @@ -136,11 +140,19 @@ class ApplicationController < ActionController::Base helper_method :set_user_domain # Checks if the user is banned and logs him out if he is - def check_if_unbanned - if current_user&.has_role?(:denied) + def check_user_role + if current_user&.has_role? :denied session.delete(:user_id) - redirect_to unauthorized_path + redirect_to root_path, flash: { alert: I18n.t("registration.banned.fail") } + elsif current_user&.has_role? :pending + session.delete(:user_id) + redirect_to root_path, flash: { alert: I18n.t("registration.approval.fail") } end end - helper_method :check_if_unbanned + helper_method :check_user_role + + # Manually Handle BigBlueButton errors + def handle_bigbluebutton_error + render "errors/bigbluebutton_error" + end end diff --git a/app/controllers/concerns/emailer.rb b/app/controllers/concerns/emailer.rb index 53a10a9c..0247296c 100644 --- a/app/controllers/concerns/emailer.rb +++ b/app/controllers/concerns/emailer.rb @@ -21,22 +21,85 @@ module Emailer # Sends account activation email. def send_activation_email(user) + return unless Rails.configuration.enable_email_verification + @user = user UserMailer.verify_email(@user, user_verification_link, logo_image, user_color).deliver end # Sends password reset email. def send_password_reset_email(user) + return unless Rails.configuration.enable_email_verification + @user = user UserMailer.password_reset(@user, reset_link, logo_image, user_color).deliver_now end + def send_user_promoted_email(user) + return unless Rails.configuration.enable_email_verification + + UserMailer.user_promoted(user, root_url, logo_image, user_color).deliver_now + end + + def send_user_demoted_email(user) + return unless Rails.configuration.enable_email_verification + + UserMailer.user_demoted(user, root_url, logo_image, user_color).deliver_now + end + + # Sends inivitation to join + def send_invitation_email(name, email, token) + return unless Rails.configuration.enable_email_verification + + @token = token + UserMailer.invite_email(name, email, invitation_link, logo_image, user_color).deliver_now + end + + def send_user_approved_email(user) + return unless Rails.configuration.enable_email_verification + + UserMailer.approve_user(user, root_url, logo_image, user_color).deliver_now + end + + def send_approval_user_signup_email(user) + return unless Rails.configuration.enable_email_verification + + UserMailer.approval_user_signup(user, admins_url, logo_image, user_color, admin_emails).deliver_now + end + + def send_invite_user_signup_email(user) + return unless Rails.configuration.enable_email_verification + + UserMailer.invite_user_signup(user, admins_url, logo_image, user_color, admin_emails).deliver_now + end + + private + # Returns the link the user needs to click to verify their account def user_verification_link - request.base_url + edit_account_activation_path(token: @user.activation_token, email: @user.email) + edit_account_activation_url(token: @user.activation_token, email: @user.email) + end + + def admin_emails + admins = User.with_role(:admin) + + if Rails.configuration.loadbalanced_configuration + admins = admins.without_role(:super_admin) + .where(provider: user_settings_provider) + end + + admins.collect(&:email).join(",") end def reset_link - request.base_url + edit_password_reset_path(@user.reset_token, email: @user.email) + edit_password_reset_url(@user.reset_token, email: @user.email) + end + + def invitation_link + if allow_greenlight_users? + signup_url(invite_token: @token) + else + root_url(invite_token: @token) + end end end diff --git a/app/controllers/concerns/registrar.rb b/app/controllers/concerns/registrar.rb new file mode 100644 index 00000000..79c0a622 --- /dev/null +++ b/app/controllers/concerns/registrar.rb @@ -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 . + +module Registrar + extend ActiveSupport::Concern + + def registration_method + Setting.find_or_create_by!(provider: user_settings_provider).get_value("Registration Method") + end + + def open_registration + registration_method == Rails.configuration.registration_methods[:open] + end + + def approval_registration + registration_method == Rails.configuration.registration_methods[:approval] + end + + def invite_registration + registration_method == Rails.configuration.registration_methods[:invite] + end + + # Returns a hash containing whether the user has been invited and if they + # signed up with the same email that they were invited with + def check_user_invited(email, token, domain) + return { present: true, verified: false } unless invite_registration + return { present: false, verified: false } if token.nil? + + invite = Invitation.valid.find_by(invite_token: token, provider: domain) + if invite.present? + # Check if they used the same email to sign up + same_email = email.casecmp(invite.email).zero? + invite.destroy + { present: true, verified: same_email } + else + { present: false, verified: false } + end + end +end diff --git a/app/controllers/concerns/themer.rb b/app/controllers/concerns/themer.rb new file mode 100644 index 00000000..97eb6b80 --- /dev/null +++ b/app/controllers/concerns/themer.rb @@ -0,0 +1,47 @@ +# 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 . + +module Themer + extend ActiveSupport::Concern + + # Lightens a color by 40% + def color_lighten(color) + # Uses the built in Sass Engine to lighten the color + + dummy_scss = "h1 { color: $lighten; }" + compiled = Sass::Engine.new("$lighten:lighten(#{color}, 40%);" + dummy_scss, syntax: :scss).render + + string_locater = 'color: ' + color_start = compiled.index(string_locater) + string_locater.length + + compiled[color_start..color_start + 6] + end + + # Darkens a color by 10% + def color_darken(color) + # Uses the built in Sass Engine to darken the color + + dummy_scss = "h1 { color: $darken; }" + compiled = Sass::Engine.new("$darken:darken(#{color}, 10%);" + dummy_scss, syntax: :scss).render + + string_locater = 'color: ' + color_start = compiled.index(string_locater) + string_locater.length + + compiled[color_start..color_start + 6] + end +end diff --git a/app/controllers/main_controller.rb b/app/controllers/main_controller.rb index a6ac81bc..df97a090 100644 --- a/app/controllers/main_controller.rb +++ b/app/controllers/main_controller.rb @@ -17,7 +17,10 @@ # with BigBlueButton; if not, see . class MainController < ApplicationController + include Registrar # GET / def index + # Store invite token + session[:invite_token] = params[:invite_token] if params[:invite_token] && invite_registration end end diff --git a/app/controllers/rooms_controller.rb b/app/controllers/rooms_controller.rb index 1918474d..b96166da 100644 --- a/app/controllers/rooms_controller.rb +++ b/app/controllers/rooms_controller.rb @@ -98,6 +98,9 @@ class RoomsController < ApplicationController # POST /:room_uid def join + return redirect_to root_path, + flash: { alert: I18n.t("administrator.site_settings.authentication.user-info") } if auth_required + opts = default_meeting_options unless @room.owned_by?(current_user) # Assign join name if passed. @@ -271,4 +274,9 @@ class RoomsController < ApplicationController def verify_user_not_admin redirect_to admins_path if current_user && current_user&.has_role?(:super_admin) end + + def auth_required + Setting.find_or_create_by!(provider: user_settings_provider).get_value("Room Authentication") == "true" && + current_user.nil? + end end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index 0dc85d40..4f6abdd0 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -17,6 +17,9 @@ # with BigBlueButton; if not, see . class SessionsController < ApplicationController + include Registrar + include Emailer + skip_before_action :verify_authenticity_token, only: [:omniauth, :fail] # GET /users/logout @@ -32,11 +35,11 @@ class SessionsController < ApplicationController user = admin else user = User.find_by(email: session_params[:email], provider: @user_domain) - redirect_to(root_path, alert: I18n.t("invalid_user")) && return unless user + redirect_to(signin_path, alert: I18n.t("invalid_user")) && return unless user redirect_to(root_path, alert: I18n.t("invalid_login_method")) && return unless user.greenlight_account? redirect_to(account_activation_path(email: user.email)) && return unless user.activated? end - redirect_to(root_path, alert: I18n.t("invalid_credentials")) && return unless user.try(:authenticate, + redirect_to(signin_path, alert: I18n.t("invalid_credentials")) && return unless user.try(:authenticate, session_params[:password]) login(user) @@ -44,11 +47,33 @@ class SessionsController < ApplicationController # GET/POST /auth/:provider/callback def omniauth - user = User.from_omniauth(request.env['omniauth.auth']) - login(user) - rescue => e - logger.error "Error authenticating via omniauth: #{e}" - omniauth_fail + begin + @auth = request.env['omniauth.auth'] + @user_exists = check_user_exists + + # If using invitation registration method, make sure user is invited + return redirect_to root_path, flash: { alert: I18n.t("registration.invite.no_invite") } unless passes_invite_reqs + + user = User.from_omniauth(@auth) + + # Add pending role if approval method and is a new user + if approval_registration && !@user_exists + user.add_role :pending + + # Inform admins that a user signed up if emails are turned on + send_approval_user_signup_email(user) if Rails.configuration.enable_email_verification + + return redirect_to root_path, flash: { success: I18n.t("registration.approval.signup") } + end + + send_invite_user_signup_email(user) if Rails.configuration.enable_email_verification && + invite_registration && !@user_exists + + login(user) + rescue => e + logger.error "Error authenticating via omniauth: #{e}" + omniauth_fail + end end # POST /auth/failure @@ -61,4 +86,17 @@ class SessionsController < ApplicationController def session_params params.require(:session).permit(:email, :password) end + + def check_user_exists + provider = @auth['provider'] == "bn_launcher" ? @auth['info']['customer'] : @auth['provider'] + User.exists?(social_uid: @auth['uid'], provider: provider) + end + + # Check if the user already exists, if not then check for invitation + def passes_invite_reqs + return true if @user_exists + + invitation = check_user_invited("", session[:invite_token], @user_domain) + invitation[:present] + end end diff --git a/app/controllers/themes_controller.rb b/app/controllers/themes_controller.rb index 812a3aa4..2f26c7f3 100644 --- a/app/controllers/themes_controller.rb +++ b/app/controllers/themes_controller.rb @@ -22,13 +22,16 @@ class ThemesController < ApplicationController # GET /primary def index color = @settings.get_value("Primary Color") || Rails.configuration.primary_color_default + lighten_color = @settings.get_value("Primary Color Lighten") || Rails.configuration.primary_color_lighten_default + darken_color = @settings.get_value("Primary Color Darken") || Rails.configuration.primary_color_darken_default + file_name = Rails.root.join('app', 'assets', 'stylesheets', 'utilities', '_primary_themes.scss') @file_contents = File.read(file_name) # Include the variables and covert scss file to css @compiled = Sass::Engine.new("$primary-color:#{color};" \ - "$primary-color-lighten:lighten(#{color}, 40%);" \ - "$primary-color-darken:darken(#{color}, 10%);" + + "$primary-color-lighten:#{lighten_color};" \ + "$primary-color-darken:#{darken_color};" + @file_contents, syntax: :scss).render respond_to do |format| diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 026bb3e2..4b2ba771 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -20,6 +20,7 @@ class UsersController < ApplicationController include RecordingsHelper include Pagy::Backend include Emailer + include Registrar before_action :find_user, only: [:edit, :update, :destroy] before_action :ensure_unauthenticated, only: [:new, :create] @@ -32,29 +33,31 @@ class UsersController < ApplicationController @user = User.new(user_params) @user.provider = @user_domain - # Add validation errors to model if they exist - valid_user = @user.valid? - valid_captcha = Rails.configuration.recaptcha_enabled ? verify_recaptcha(model: @user) : true + # User or recpatcha is not valid + render(:new) && return unless valid_user_or_captcha - if valid_user && valid_captcha - @user.save - else - render(:new) && return + # Redirect to root if user token is either invalid or expired + return redirect_to root_path, flash: { alert: I18n.t("registration.invite.fail") } unless passes_invite_reqs + + # User has passed all validations required + @user.save + + # Set user to pending and redirect if Approval Registration is set + if approval_registration + @user.add_role :pending + + return redirect_to root_path, + flash: { success: I18n.t("registration.approval.signup") } unless Rails.configuration.enable_email_verification end - # Sign in automatically if email verification is disabled. - login(@user) && return unless Rails.configuration.enable_email_verification + send_registration_email if Rails.configuration.enable_email_verification - # Start email verification and redirect to root. - begin - send_activation_email(@user) - rescue => e - logger.error "Error in email delivery: #{e}" - flash[:alert] = I18n.t(params[:message], default: I18n.t("delivery_error")) - else - flash[:success] = I18n.t("email_sent", email_type: t("verify.verification")) - end - redirect_to(root_path) + # Sign in automatically if email verification is disabled or if user is already verified. + login(@user) && return if !Rails.configuration.enable_email_verification || @user.email_verified + + send_verification + + redirect_to root_path end # GET /signin @@ -63,11 +66,16 @@ class UsersController < ApplicationController # GET /signup def new - if Rails.configuration.allow_user_signup - @user = User.new - else - redirect_to root_path + return redirect_to root_path unless Rails.configuration.allow_user_signup + + # Check if the user needs to be invited + if invite_registration + redirect_to root_path, flash: { alert: I18n.t("registration.invite.no_invite") } unless params[:invite_token] + + session[:invite_token] = params[:invite_token] end + + @user = User.new end # GET /u/:user_uid/edit @@ -174,4 +182,47 @@ class UsersController < ApplicationController params.require(:user).permit(:name, :email, :image, :password, :password_confirmation, :new_password, :provider, :accepted_terms, :language) end + + def send_verification + # Start email verification and redirect to root. + begin + send_activation_email(@user) + rescue => e + logger.error "Error in email delivery: #{e}" + flash[:alert] = I18n.t(params[:message], default: I18n.t("delivery_error")) + else + flash[:success] = I18n.t("email_sent", email_type: t("verify.verification")) + end + end + + def send_registration_email + begin + if invite_registration + send_invite_user_signup_email(@user) + elsif approval_registration + send_approval_user_signup_email(@user) + end + rescue => e + logger.error "Error in email delivery: #{e}" + flash[:alert] = I18n.t(params[:message], default: I18n.t("delivery_error")) + end + end + + # Add validation errors to model if they exist + def valid_user_or_captcha + valid_user = @user.valid? + valid_captcha = Rails.configuration.recaptcha_enabled ? verify_recaptcha(model: @user) : true + + valid_user && valid_captcha + end + + # Checks if the user passes the requirements to be invited + def passes_invite_reqs + # check if user needs to be invited and IS invited + invitation = check_user_invited(@user.email, session[:invite_token], @user_domain) + + @user.email_verified = true if invitation[:verified] + + invitation[:present] + end end diff --git a/app/helpers/admins_helper.rb b/app/helpers/admins_helper.rb index e0aa5c5c..ab430e71 100644 --- a/app/helpers/admins_helper.rb +++ b/app/helpers/admins_helper.rb @@ -18,4 +18,39 @@ module AdminsHelper include Pagy::Frontend + + def display_invite + current_page?(admins_path) && invite_registration + end + + def registration_method + Setting.find_or_create_by!(provider: user_settings_provider).get_value("Registration Method") + end + + def invite_registration + registration_method == Rails.configuration.registration_methods[:invite] + end + + def approval_registration + registration_method == Rails.configuration.registration_methods[:approval] + end + + def room_authentication_string + if Setting.find_or_create_by!(provider: user_settings_provider).get_value("Room Authentication") == "true" + I18n.t("administrator.site_settings.authentication.enabled") + else + I18n.t("administrator.site_settings.authentication.disabled") + end + end + + def registration_method_string + case registration_method + when Rails.configuration.registration_methods[:open] + I18n.t("administrator.site_settings.registration.methods.open") + when Rails.configuration.registration_methods[:invite] + I18n.t("administrator.site_settings.registration.methods.invite") + when Rails.configuration.registration_methods[:approval] + I18n.t("administrator.site_settings.registration.methods.approval") + end + end end diff --git a/app/helpers/rooms_helper.rb b/app/helpers/rooms_helper.rb index a1991872..e355a8e5 100644 --- a/app/helpers/rooms_helper.rb +++ b/app/helpers/rooms_helper.rb @@ -22,4 +22,9 @@ module RoomsHelper def google_calendar_path "http://calendar.google.com/calendar/r/eventedit?text=#{@room.name}&location=#{request.base_url + request.fullpath}" end + + def room_authentication_required + Setting.find_or_create_by!(provider: user_settings_provider).get_value("Room Authentication") == "true" && + current_user.nil? + end end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index 3ffee474..cebebe92 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -34,4 +34,55 @@ class UserMailer < ApplicationMailer @color = color mail to: user.email, subject: t('reset_password.subtitle') end + + def user_promoted(user, url, image, color) + @url = url + @admin_url = url + "admins" + @image = image + @color = color + mail to: user.email, subject: t('mailer.user.promoted.subtitle') + end + + def user_demoted(user, url, image, color) + @url = url + @root_url = url + @image = image + @color = color + mail to: user.email, subject: t('mailer.user.demoted.subtitle') + end + + def invite_email(name, email, url, image, color) + @name = name + @email = email + @url = url + @image = image + @color = color + mail to: email, subject: t('mailer.user.invite.subject') + end + + def approve_user(user, url, image, color) + @user = user + @url = url + @image = image + @color = color + mail to: user.email, subject: t('mailer.user.approve.subject') + end + + def approval_user_signup(user, url, image, color, admin_emails) + @user = user + @url = url + @image = image + @color = color + + mail to: admin_emails, subject: t('mailer.user.approve.signup.subject') + end + + def invite_user_signup(user, url, image, color, admin_emails) + @user = user + @url = url + @image = image + @color = color + + mail to: admin_emails, subject: t('mailer.user.invite.signup.subject') + end end diff --git a/app/models/invitation.rb b/app/models/invitation.rb new file mode 100644 index 00000000..b1e36afb --- /dev/null +++ b/app/models/invitation.rb @@ -0,0 +1,23 @@ +# 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 . + +class Invitation < ApplicationRecord + has_secure_token :invite_token + + scope :valid, -> { where(updated_at: (Time.now - 48.hours)..Time.now) } +end diff --git a/app/models/setting.rb b/app/models/setting.rb index c0e859e3..b4f07ff0 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -37,6 +37,10 @@ class Setting < ApplicationRecord Rails.configuration.branding_image_default when "Primary Color" Rails.configuration.primary_color_default + when "Registration Method" + Rails.configuration.registration_method_default + when "Room Authentication" + false end end end diff --git a/app/models/user.rb b/app/models/user.rb index 06534269..2966e22e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -111,8 +111,8 @@ class User < ApplicationRecord "created_at" end - search_query = "name LIKE :search OR email LIKE :search OR username LIKE :search" \ - " OR #{created_at_query} LIKE :search OR provider LIKE :search" + search_query = "users.name LIKE :search OR email LIKE :search OR username LIKE :search" \ + " OR users.#{created_at_query} LIKE :search OR provider LIKE :search" search_param = "%#{string}%" where(search_query, search: search_param) end diff --git a/app/views/errors/bigbluebutton_error.html.erb b/app/views/errors/bigbluebutton_error.html.erb new file mode 100644 index 00000000..b1e41b6b --- /dev/null +++ b/app/views/errors/bigbluebutton_error.html.erb @@ -0,0 +1,20 @@ +<% +# 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 . +%> + +
+
<%= I18n.t("errors.bigbluebutton.title") %>
+

<%= I18n.t("errors.bigbluebutton.message") %>

+

<%= I18n.t("errors.bigbluebutton.help", doc_link: "http://docs.bigbluebutton.org/greenlight/gl-install.html#setting-bigbluebutton-credentials").html_safe %>

+
diff --git a/app/views/errors/unauthorized.html.erb b/app/views/errors/unauthorized.html.erb index e54591d7..1e6682a8 100644 --- a/app/views/errors/unauthorized.html.erb +++ b/app/views/errors/unauthorized.html.erb @@ -15,6 +15,6 @@
401
-

<%= t("errors.unauthorized.message") %>

-

<%= t("errors.unauthorized.help") %>

+

<%= I18n.t("errors.unauthorized.message") %>

+

<%= I18n.t("errors.unauthorized.help") %>

diff --git a/app/views/rooms/join.html.erb b/app/views/rooms/join.html.erb index 70c74c82..5e718e15 100644 --- a/app/views/rooms/join.html.erb +++ b/app/views/rooms/join.html.erb @@ -14,18 +14,22 @@ %> <%= render 'shared/room_event' do %> - <%= form_for room_path(@room), method: :post do |f| %> -
- <%= f.hidden_field(:search, :value => params[:search])%> - <%= f.hidden_field(:column, :value => params[:column])%> - <%= f.hidden_field(:direction, :value => params[:direction])%> - <%= f.text_field :join_name, - required: true, - class: "form-control join-form", - placeholder: t("enter_your_name"), - value: "#{@name}", - readonly: !current_user.nil? %> - <%= f.submit t("room.join"), class: "btn btn-primary btn-sm col-sm-3 form-control join-form" %> -
+ <% if room_authentication_required %> +

<%= t("administrator.site_settings.authentication.user-info") %>

+ <% else %> + <%= form_for room_path(@room), method: :post do |f| %> +
+ <%= f.hidden_field(:search, :value => params[:search])%> + <%= f.hidden_field(:column, :value => params[:column])%> + <%= f.hidden_field(:direction, :value => params[:direction])%> + <%= f.text_field :join_name, + required: true, + class: "form-control join-form", + placeholder: t("enter_your_name"), + value: "#{@name}", + readonly: !current_user.nil? %> + <%= f.submit t("room.join"), class: "btn btn-primary btn-sm col-sm-3 form-control join-form" %> +
+ <% end %> <% end %> <% end %> diff --git a/app/views/shared/admin_settings/_site_settings.html.erb b/app/views/shared/admin_settings/_site_settings.html.erb index 793083db..62d124f1 100644 --- a/app/views/shared/admin_settings/_site_settings.html.erb +++ b/app/views/shared/admin_settings/_site_settings.html.erb @@ -16,7 +16,7 @@
-
+
@@ -26,22 +26,77 @@
- -
+
+
+
+
+
-
-
- - -
- - - +
+ + + + +
+ <%= t("administrator.site_settings.color.regular") %> +
+ +
+ <%= t("administrator.site_settings.color.lighten") %> +
+ +
+ <%= t("administrator.site_settings.color.darken") %>
+
+
+
+ + + +
+
+
+
+
+
+ + + +
+
+
diff --git a/app/views/shared/admin_settings/_users.html.erb b/app/views/shared/admin_settings/_users.html.erb index 47140dd5..f0ce668c 100644 --- a/app/views/shared/admin_settings/_users.html.erb +++ b/app/views/shared/admin_settings/_users.html.erb @@ -13,6 +13,10 @@ # with BigBlueButton; if not, see . %> +<% if @role.present? %> + <%= render "shared/components/admins_tags" %> +<% end %> +
@@ -68,27 +72,25 @@ <%= user.email && user.email != "" ? user.email : user.username%> <%= user.provider %> - <% roles = user.roles().pluck(:name) %> - <% if roles.include?("denied")%> -
- <%= t("roles.banned") %> -
- <% elsif roles.include?("super_admin") %> -
- <%= t("roles.super_admin") %> -
- <% elsif roles.include?("admin") %> -
- <%= t("roles.administrator") %> -
- <% else %> -
- <%= t("roles.user") %> -
- <% end %> + <% roles = user.roles().pluck(:name) %> + <%= render "shared/components/admins_role", roles: roles %> - <% unless roles.include?("super_admin") %> + <% if roles.include?("pending") %> + + <% elsif !roles.include?("super_admin") %>
+ +<%= render "shared/modals/invite_user_modal" %> diff --git a/app/views/shared/components/_admins_role.html.erb b/app/views/shared/components/_admins_role.html.erb new file mode 100644 index 00000000..baa42abb --- /dev/null +++ b/app/views/shared/components/_admins_role.html.erb @@ -0,0 +1,36 @@ +<% +# 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 . +%> + +<% if roles.include?("denied")%> + +<% elsif roles.include?("pending") %> + +<% elsif roles.include?("super_admin") %> + +<% elsif roles.include?("admin") %> + +<% else %> + +<% end %> \ No newline at end of file diff --git a/app/views/shared/components/_admins_tags.html.erb b/app/views/shared/components/_admins_tags.html.erb new file mode 100644 index 00000000..a1e6142d --- /dev/null +++ b/app/views/shared/components/_admins_tags.html.erb @@ -0,0 +1,57 @@ +<% +# 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 . +%> + +
+
+
+ <% if @role == "denied"%> + + <%= t("roles.banned") %> + + + + + <% elsif @role == "pending" %> + + <%= t("roles.pending") %> + + + + + <% elsif @role == "super_admin" %> + + <%= t("roles.super_admin") %> + + + + + <% elsif @role == "admin" %> + + <%= t("roles.administrator") %> + + + + + <% else %> + + <%= t("roles.user") %> + + + + + <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/shared/components/_subtitle.html.erb b/app/views/shared/components/_subtitle.html.erb index 29870f89..f051494a 100644 --- a/app/views/shared/components/_subtitle.html.erb +++ b/app/views/shared/components/_subtitle.html.erb @@ -14,12 +14,20 @@ %>
-
+

<%= subtitle %>

<% if search %> -
-