From 0cc3b2bd6290b17904d19a922b9ae681c20e3318 Mon Sep 17 00:00:00 2001 From: Bilal Date: Wed, 12 Aug 2020 13:52:22 +0200 Subject: [PATCH 1/7] Get code and token from Azure AD and store it to the DB --- Gemfile | 7 +- Gemfile.lock | 9 +- app/controllers/callbacks_controller.rb | 12 +- app/views/profiles/show.html.erb | 2 +- config/initializers/omniauth.rb | 29 ++--- config/routes.rb | 2 +- ...810140331_add_microsoft_tokens_to_users.rb | 7 + db/structure.sql | 8 +- lib/azure_ad.rb | 121 ++++++++++++++++++ 9 files changed, 171 insertions(+), 26 deletions(-) create mode 100644 db/migrate/20200810140331_add_microsoft_tokens_to_users.rb create mode 100644 lib/azure_ad.rb diff --git a/Gemfile b/Gemfile index 098be61..425a27b 100644 --- a/Gemfile +++ b/Gemfile @@ -139,9 +139,14 @@ gem 'rack-cors' # Ruby wrappers for the HubSpot REST API gem "hubspot-ruby" +# OAuth +gem 'omniauth-oauth2', '~> 1.6' +# OmniAuth CSRF protection +gem 'omniauth-rails_csrf_protection', '~> 0.1.2' + # authenticate via Microsoft # gem 'omniauth-microsoft_graph', git: 'https://github.com/m4c3/omniauth-microsoft_graph' -gem 'omniauth-microsoft_graph' +# gem 'omniauth-microsoft_graph' group :development, :test, :review do # Call "byebug" anywhere in the code to stop execution and get a debugger console diff --git a/Gemfile.lock b/Gemfile.lock index 15ae75b..34bba69 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -319,12 +319,12 @@ GEM omniauth (1.9.1) hashie (>= 3.4.6) rack (>= 1.6.2, < 3) - omniauth-microsoft_graph (0.3.3) - omniauth (~> 1.1, >= 1.1.1) - omniauth-oauth2 (~> 1.6) omniauth-oauth2 (1.6.0) oauth2 (~> 1.1) omniauth (~> 1.9) + omniauth-rails_csrf_protection (0.1.2) + actionpack (>= 4.2) + omniauth (>= 1.3.1) parallel (1.19.1) parity (3.2.0) parser (2.6.5.0) @@ -569,7 +569,8 @@ DEPENDENCIES mux_ruby! oath (~> 1.1.0) oath-generators (~> 1.0.1) - omniauth-microsoft_graph + omniauth-oauth2 (~> 1.6) + omniauth-rails_csrf_protection (~> 0.1.2) parity (~> 3.2.0) pdf-reader (~> 2.1.0) pdfkit (~> 0.8.2) diff --git a/app/controllers/callbacks_controller.rb b/app/controllers/callbacks_controller.rb index 2f727ae..b3c4989 100644 --- a/app/controllers/callbacks_controller.rb +++ b/app/controllers/callbacks_controller.rb @@ -5,6 +5,16 @@ class CallbacksController < ApplicationController skip_before_action :verify_authenticity_token def create - render plain: params.inspect + token_data = request.env['omniauth.auth'][:credentials] + + current_user&.tap do |user| + user.microsoft_access_token = token_data.token + user.microsoft_refresh_token = token_data.refresh_token + user.microsoft_token_expires_at = token_data.expires_at # Expiration time is returned in seconds + user.save + end + + redirect_to profile_path end + end diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 2f4775b..d677cab 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -17,7 +17,7 @@ <%= @user.role_for(Current.account).to_s.titleize %> <% end %>

- <%= link_to 'Auth to Microsoft', '/auth/microsoft_graph', class: "btn btn-primary" %> + <%= link_to 'Auth to Microsoft', '/auth/azure_ad', method: :post, class: "btn btn-primary" %>
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index d85c6e7..07e6abc 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -1,20 +1,17 @@ -ENV['AZURE_CLIENT_ID'] = 'c45b93ae-ef07-415d-b13a-ab566b877c1c' -ENV['AZURE_CLIENT_SECRET'] = 'XVboF2sRaS_H2oK6I9R56.A_exnRhiv~Xt' -ENV['AZURE_TENANT_ID'] = '1e33d1c7-dfb4-4df1-86da-62770313bcb0' -ENV['AZURE_EXTENSIONS'] = '' - -# Rails.application.config.middleware.use OmniAuth::Builder do -# provider :microsoft_graph,{ -# client_id: ENV['AZURE_CLIENT_ID'], -# client_secret: ENV['AZURE_CLIENT_SECRET'], -# tenant_id: ENV['AZURE_TENANT_ID'], -# extensions: ENV['AZURE_EXTENSIONS'], -# redirect_uri: 'https://517e57c6cd6c.ngrok.io/auth/microsoft_graph/callback', -# scope: 'openid email profile User.Read' -# } -# end +require 'azure_ad' Rails.application.config.middleware.use OmniAuth::Builder do - provider :microsoft_graph, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], scope: 'openid email profile User.Read' + provider :azure_ad, + client_id: ENV['AZURE_CLIENT_ID'], + client_secret: ENV['AZURE_CLIENT_SECRET'], + redirect_uri: ENV['AZURE_REDIRECT_URI'], + client_options: { + token_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/token", + authorize_url: "#{ENV['AZURE_TENANT_ID']}/oauth2/v2.0/authorize" + }, + scope: ENV['AZURE_SCOPES'] end +# Rails.application.config.middleware.use OmniAuth::Builder do +# provider :microsoft_graph, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], scope: 'openid email profile User.Read' +# end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index e18fa00..32bf489 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,7 +4,7 @@ require 'sidekiq/web' Rails.application.routes.draw do AVAILABLE_LOCALES_REGEX = /#{I18n.available_locales.join("|")}/.freeze - get 'auth/microsoft_graph/callback', to: 'callbacks#create' + get 'auth/azure_ad/callback', to: 'callbacks#create' concern :confirmable do resources :video_release_confirmations, only: [:new, :create, :destroy] diff --git a/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb b/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb new file mode 100644 index 0000000..c58be13 --- /dev/null +++ b/db/migrate/20200810140331_add_microsoft_tokens_to_users.rb @@ -0,0 +1,7 @@ +class AddMicrosoftTokensToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :microsoft_access_token, :string + add_column :users, :microsoft_refresh_token, :string + add_column :users, :microsoft_token_expires_at, :integer + end +end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index fce0e15..d5a9301 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1755,7 +1755,10 @@ CREATE TABLE public.users ( remember_created_at timestamp without time zone, first_name character varying, last_name character varying, - time_zone character varying DEFAULT 'UTC'::character varying NOT NULL + time_zone character varying DEFAULT 'UTC'::character varying NOT NULL, + microsoft_access_token character varying, + microsoft_refresh_token character varying, + microsoft_token_expires_at integer ); @@ -3966,6 +3969,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200720051634'), ('20200720131309'), ('20200721140821'), -('20200725231419'); +('20200725231419'), +('20200810140331'); diff --git a/lib/azure_ad.rb b/lib/azure_ad.rb new file mode 100644 index 0000000..1899dd0 --- /dev/null +++ b/lib/azure_ad.rb @@ -0,0 +1,121 @@ +require 'omniauth-oauth2' + +module OmniAuth + module Strategies + class AzureAd < OmniAuth::Strategies::OAuth2 + BASE_SCOPE_URL = 'https://graph.microsoft.com/' + BASE_SCOPES = %w[offline_access openid email profile].freeze + DEFAULT_SCOPE = 'offline_access openid email profile User.Read'.freeze + + option :name, :azure_ad + + option :client_options, + site: 'https://login.microsoftonline.com/' + + option :authorize_options, %i[state callback_url access_type auth_type scope prompt login_hint domain_hint response_mode] + + option :token_params, {} + + option :scope, DEFAULT_SCOPE + option :authorized_client_ids, [] + + uid { raw_info["id"] } + + info do + { + # 'email' => raw_info["mail"], + # 'first_name' => raw_info["givenName"], + # 'last_name' => raw_info["surname"], + # 'name' => [raw_info["givenName"], raw_info["surname"]].join(' '), + # 'nickname' => raw_info["displayName"], + } + end + + extra do + { + # 'raw_info' => raw_info, + # 'params' => access_token.params, + # 'aud' => options.client_id + } + end + + def authorize_params + super.tap do |params| + options[:authorize_options].each do |k| + params[k] = request.params[k.to_s] unless [nil, ''].include?(request.params[k.to_s]) + end + + params[:scope] = get_scope(params) + + session['omniauth.state'] = params[:state] if params[:state] + end + end + + def raw_info + @raw_info ||= access_token.get('https://graph.microsoft.com/v1.0/me').parsed + end + + def callback_url + options[:callback_url] || full_host + script_name + callback_path + end + + def custom_build_access_token + token_response = get_access_token(request) + session[:microsoft_graph_api_token] = token_response.token + token_response + end + + alias build_access_token custom_build_access_token + + private + + def get_access_token(request) + verifier = request.params['code'] + redirect_uri = request.params['redirect_uri'] || request.params['callback_url'] + if verifier && request.xhr? + client_get_token(verifier, redirect_uri || '/auth/azure_ad/callback') + elsif verifier + client_get_token(verifier, redirect_uri || callback_url) + elsif verify_token(request.params['access_token']) + ::OAuth2::AccessToken.from_hash(client, request.params.dup) + elsif request.content_type =~ /json/i + begin + body = JSON.parse(request.body.read) + request.body.rewind # rewind request body for downstream middlewares + verifier = body && body['code'] + client_get_token(verifier, '/auth/azure_ad/callback') if verifier + rescue JSON::ParserError => e + warn "[omniauth google-oauth2] JSON parse error=#{e}" + end + end + end + + def client_get_token(verifier, redirect_uri) + client.auth_code.get_token(verifier, get_token_options(redirect_uri), get_token_params) + end + + def get_token_params + deep_symbolize(options.auth_token_params || {}) + end + + def get_token_options(redirect_uri = '') + { redirect_uri: redirect_uri }.merge(token_params.to_hash(symbolize_keys: true)) + end + + def get_scope(params) + raw_scope = params[:scope] || DEFAULT_SCOPE + scope_list = raw_scope.split(' ').map { |item| item.split(',') }.flatten + scope_list.map! { |s| s =~ %r{^https?://} || BASE_SCOPES.include?(s) ? s : "#{BASE_SCOPE_URL}#{s}" } + scope_list.join(' ') + end + + def verify_token(access_token) + return false unless access_token + # access_token.get('https://graph.microsoft.com/v1.0/me').parsed + raw_response = client.request(:get, 'https://graph.microsoft.com/v1.0/me', + params: { access_token: access_token }).parsed + (raw_response['aud'] == options.client_id) || options.authorized_client_ids.include?(raw_response['aud']) + end + end + end +end \ No newline at end of file -- 2.47.3 From 3352217a0311958b798591ad1a308be49027f23e Mon Sep 17 00:00:00 2001 From: Bilal Date: Wed, 12 Aug 2020 14:44:22 +0200 Subject: [PATCH 2/7] update azure_ad strategy --- lib/azure_ad.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/azure_ad.rb b/lib/azure_ad.rb index 1899dd0..b79a410 100644 --- a/lib/azure_ad.rb +++ b/lib/azure_ad.rb @@ -1,5 +1,8 @@ require 'omniauth-oauth2' +# This file is from omniauth-microsoft_graph lib (not installed) +# It is modified to make auth work + module OmniAuth module Strategies class AzureAd < OmniAuth::Strategies::OAuth2 @@ -12,7 +15,7 @@ module OmniAuth option :client_options, site: 'https://login.microsoftonline.com/' - option :authorize_options, %i[state callback_url access_type auth_type scope prompt login_hint domain_hint response_mode] + option :authorize_options, %i[state callback_url scope response_mode] option :token_params, {} -- 2.47.3 From f25a72004efc6e55ce4ecfded4c94847272c0c52 Mon Sep 17 00:00:00 2001 From: Bilal Date: Wed, 12 Aug 2020 16:36:47 +0200 Subject: [PATCH 3/7] create graph api client to handle requests --- .env.sample | 8 ++ app/controllers/broadcasts_controller.rb | 3 +- app/controllers/callbacks_controller.rb | 2 + .../microsoft_teams_meetings_controller.rb | 17 ++++ config/routes.rb | 3 +- ...12161119_add_microsoft_user_id_to_users.rb | 5 ++ db/structure.sql | 6 +- lib/microsoft_graph.rb | 83 +++++++++++++++++++ 8 files changed, 123 insertions(+), 4 deletions(-) create mode 100644 app/controllers/microsoft_teams_meetings_controller.rb create mode 100644 db/migrate/20200812161119_add_microsoft_user_id_to_users.rb create mode 100644 lib/microsoft_graph.rb diff --git a/.env.sample b/.env.sample index c506d1a..905a375 100644 --- a/.env.sample +++ b/.env.sample @@ -28,3 +28,11 @@ MUX_TOKEN_ID= MUX_TOKEN_SECRET= MUX_BROADCAST_SERVER_URL=rtmp://global-live.mux.com:5222/app MUX_TEST_MODE_DISABLED= + + +# Required for Microsoft Azure AD Auth +AZURE_CLIENT_ID = Client App ID +AZURE_CLIENT_SECRET = Client App Secret +AZURE_TENANT_ID = Client App Tenant ID +AZURE_REDIRECT_URI = where microsoft will redirect after login, eg. http://localhost:3000/auth/azure_ad/callback +AZURE_SCOPES = Scopes required for Application, eg. 'openid email profile User.Read offline_access OnlineMeetings.ReadWrite' \ No newline at end of file diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index 072498b..3a6a1ba 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -26,7 +26,8 @@ class BroadcastsController < ApplicationController end def show - @conference_url = url_for [@broadcast.project, @broadcast, :zoom_meeting] + # @conference_url = url_for [@broadcast.project, @broadcast, :zoom_meeting] + @conference_url = url_for [@broadcast.project, @broadcast, :microsoft_teams_meeting] @recordings = @broadcast.broadcast_recordings.order_by_recent.paginate(page: params[:page]) @files = @broadcast.files.order("created_at DESC").paginate(page: params[:files_page]) render layout: 'application' diff --git a/app/controllers/callbacks_controller.rb b/app/controllers/callbacks_controller.rb index b3c4989..c08d9a4 100644 --- a/app/controllers/callbacks_controller.rb +++ b/app/controllers/callbacks_controller.rb @@ -5,9 +5,11 @@ class CallbacksController < ApplicationController skip_before_action :verify_authenticity_token def create + uid = request.env['omniauth.auth'][:uid] token_data = request.env['omniauth.auth'][:credentials] current_user&.tap do |user| + user.microsoft_user_id = uid user.microsoft_access_token = token_data.token user.microsoft_refresh_token = token_data.refresh_token user.microsoft_token_expires_at = token_data.expires_at # Expiration time is returned in seconds diff --git a/app/controllers/microsoft_teams_meetings_controller.rb b/app/controllers/microsoft_teams_meetings_controller.rb new file mode 100644 index 0000000..c86bfc2 --- /dev/null +++ b/app/controllers/microsoft_teams_meetings_controller.rb @@ -0,0 +1,17 @@ +class MicrosoftTeamsMeetingsController < ApplicationController + require 'microsoft_graph' + def show + authorize broadcast = Broadcast.find(params[:broadcast_id]) + + graph_api = MicrosoftGraph.new(current_user, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], ENV['AZURE_SCOPES']) + + meeting_start = DateTime.now + meeting_end = DateTime.now + 1.hour + subject = "Broadcast Meeting" + + + r = graph_api.create_teams_meeting(meeting_start, meeting_end, subject) + + render plain: r + end +end \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 32bf489..327a924 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,7 +101,8 @@ Rails.application.routes.draw do member do delete :destroy_file end - resource :zoom_meeting, only: [:show] + # resource :zoom_meeting, only: [:show] + resource :microsoft_teams_meeting, only: [:show] end resources :directories, except: [:index] do member do diff --git a/db/migrate/20200812161119_add_microsoft_user_id_to_users.rb b/db/migrate/20200812161119_add_microsoft_user_id_to_users.rb new file mode 100644 index 0000000..1ae14f6 --- /dev/null +++ b/db/migrate/20200812161119_add_microsoft_user_id_to_users.rb @@ -0,0 +1,5 @@ +class AddMicrosoftUserIdToUsers < ActiveRecord::Migration[6.0] + def change + add_column :users, :microsoft_user_id, :string + end +end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index d5a9301..34371f4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -1758,7 +1758,8 @@ CREATE TABLE public.users ( time_zone character varying DEFAULT 'UTC'::character varying NOT NULL, microsoft_access_token character varying, microsoft_refresh_token character varying, - microsoft_token_expires_at integer + microsoft_token_expires_at integer, + microsoft_user_id character varying ); @@ -3970,6 +3971,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200720131309'), ('20200721140821'), ('20200725231419'), -('20200810140331'); +('20200810140331'), +('20200812161119'); diff --git a/lib/microsoft_graph.rb b/lib/microsoft_graph.rb new file mode 100644 index 0000000..040d22c --- /dev/null +++ b/lib/microsoft_graph.rb @@ -0,0 +1,83 @@ +require 'httparty' + +class MicrosoftGraph + BASE_URL = 'https://graph.microsoft.com/v1.0'.freeze + BETA_BASE_URL = 'https://graph.microsoft.com/beta'.freeze + # Documentation says that "common" can be used (instead of tenantID) to refresh access token + REFRESH_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'.freeze + + def initialize(current_user, client_id, client_secret, scopes) + @current_user = current_user + @uid = current_user.microsoft_user_id + @token = current_user.microsoft_access_token + @refresh_token = current_user.microsoft_refresh_token + @expires_at = current_user.microsoft_token_expires_at + + @client_id = client_id + @client_secret = client_secret + @scopes = scopes + end + + def request(resource) + return false unless @token + + response = HTTParty.get( + "#{BASE_URL}/#{resource}", + headers: { + Authorization: "Bearer #{@token}" + } + ) + + if response.code != 200 + p '[Microsoft Graph API Error]' + p response.inspect + false + else + JSON.parse(response.body) + end + end + + def create_teams_meeting(start_date_time, end_date_time, subject) + # check if token expired and obtain new access_token using refresh token + + response = HTTParty.post( + "#{BETA_BASE_URL}/me/onlineMeetings", + body: { + startDateTime: start_date_time, + endDateTime: end_date_time, + subject: subject, + participants: { + organizer: { + identity: { + user: { + id: @uid + } + } + } + } + }, + headers: { + Authorization: "Bearer #{@token}" + } + ) + + if response.code != 201 + p '[Microsoft Graph API Error] Failed to create online meeting' + p response.inspect + else + JSON.parse(response.body) + end + end + + def refresh_token + response = HTTParty.post(REFRESH_TOKEN_URL, + body: { + client_id: @client_id, + client_secret: @client_secret, + refresh_token: @refresh_token, + grant_type: 'refresh_token', + scope: @scopes + }) + + end +end \ No newline at end of file -- 2.47.3 From be5261037eb6fe68988e33269c74655a120206ec Mon Sep 17 00:00:00 2001 From: Bilal Date: Tue, 18 Aug 2020 20:43:33 +0300 Subject: [PATCH 4/7] Complete teams meeting creation and token refresh --- app/controllers/broadcasts_controller.rb | 22 ++++- .../microsoft_teams_meetings_controller.rb | 17 ---- app/models/broadcast.rb | 1 + app/views/broadcasts/show.html.erb | 2 +- config/routes.rb | 1 - ...ft_teams_meeting_join_url_to_broadcasts.rb | 5 ++ db/structure.sql | 6 +- lib/microsoft_graph.rb | 82 +++++++++++-------- 8 files changed, 80 insertions(+), 56 deletions(-) delete mode 100644 app/controllers/microsoft_teams_meetings_controller.rb create mode 100644 db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index 3a6a1ba..fa93af8 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -1,4 +1,5 @@ class BroadcastsController < ApplicationController + require 'microsoft_graph' layout "project" before_action :set_project @@ -17,6 +18,25 @@ class BroadcastsController < ApplicationController def create @broadcast.attributes = broadcast_params + begin + graph_api = MicrosoftGraph.new( + current_user, + ENV['AZURE_CLIENT_ID'], + ENV['AZURE_CLIENT_SECRET'], + ENV['AZURE_TENANT_ID'], + ENV['AZURE_SCOPES'] + ) + + subject = "#{@broadcast.name} Online Meeting" + teams_meeting = graph_api.create_teams_meeting(subject) + join_url = teams_meeting['joinUrl'] + @broadcast.microsoft_teams_meeting_url = join_url if join_url.present? + rescue StandardError => e + @broadcast.errors[:base] << e.message + render :new + return + end + if @broadcast.save log_create_analytics redirect_to [@project, :broadcasts], notice: t(".notice") @@ -27,7 +47,7 @@ class BroadcastsController < ApplicationController def show # @conference_url = url_for [@broadcast.project, @broadcast, :zoom_meeting] - @conference_url = url_for [@broadcast.project, @broadcast, :microsoft_teams_meeting] + @conference_url = @broadcast.microsoft_teams_meeting_url @recordings = @broadcast.broadcast_recordings.order_by_recent.paginate(page: params[:page]) @files = @broadcast.files.order("created_at DESC").paginate(page: params[:files_page]) render layout: 'application' diff --git a/app/controllers/microsoft_teams_meetings_controller.rb b/app/controllers/microsoft_teams_meetings_controller.rb deleted file mode 100644 index c86bfc2..0000000 --- a/app/controllers/microsoft_teams_meetings_controller.rb +++ /dev/null @@ -1,17 +0,0 @@ -class MicrosoftTeamsMeetingsController < ApplicationController - require 'microsoft_graph' - def show - authorize broadcast = Broadcast.find(params[:broadcast_id]) - - graph_api = MicrosoftGraph.new(current_user, ENV['AZURE_CLIENT_ID'], ENV['AZURE_CLIENT_SECRET'], ENV['AZURE_SCOPES']) - - meeting_start = DateTime.now - meeting_end = DateTime.now + 1.hour - subject = "Broadcast Meeting" - - - r = graph_api.create_teams_meeting(meeting_start, meeting_end, subject) - - render plain: r - end -end \ No newline at end of file diff --git a/app/models/broadcast.rb b/app/models/broadcast.rb index d1f6943..42df29b 100644 --- a/app/models/broadcast.rb +++ b/app/models/broadcast.rb @@ -1,4 +1,5 @@ class Broadcast < ApplicationRecord + require 'microsoft_graph' include PgSearch belongs_to :project diff --git a/app/views/broadcasts/show.html.erb b/app/views/broadcasts/show.html.erb index d36fccc..ccd5fc2 100644 --- a/app/views/broadcasts/show.html.erb +++ b/app/views/broadcasts/show.html.erb @@ -119,7 +119,7 @@
<% end %>

If you want to join the ZOOM meeting dedicated to this broadcast, follow the link below.

- <%= link_to 'Video Conference', @conference_url, class: 'btn btn-primary btn-block', target: '_blank' %> + <%= link_to 'Video Conference', @conference_url, class: "btn btn-primary btn-block #{@conference_url.present? ? '' : 'disabled'}", target: '_blank' %>
params[:active_tab] == 'recordings') %>" id="recordings">
diff --git a/config/routes.rb b/config/routes.rb index 327a924..5bdeac1 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -102,7 +102,6 @@ Rails.application.routes.draw do delete :destroy_file end # resource :zoom_meeting, only: [:show] - resource :microsoft_teams_meeting, only: [:show] end resources :directories, except: [:index] do member do diff --git a/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb b/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb new file mode 100644 index 0000000..ddff8e9 --- /dev/null +++ b/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb @@ -0,0 +1,5 @@ +class AddMicrosoftTeamsMeetingJoinUrlToBroadcasts < ActiveRecord::Migration[6.0] + def change + add_column :broadcasts, :microsoft_teams_meeting_url, :string + end +end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 34371f4..1707992 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -562,7 +562,8 @@ CREATE TABLE public.broadcasts ( token character varying, streamer_status integer DEFAULT 0, shoot_location_time_zone character varying DEFAULT 'UTC'::character varying, - full_live_stream_playback_uid character varying + full_live_stream_playback_uid character varying, + microsoft_teams_meeting_url character varying ); @@ -3972,6 +3973,7 @@ INSERT INTO "schema_migrations" (version) VALUES ('20200721140821'), ('20200725231419'), ('20200810140331'), -('20200812161119'); +('20200812161119'), +('20200817233053'); diff --git a/lib/microsoft_graph.rb b/lib/microsoft_graph.rb index 040d22c..586eea6 100644 --- a/lib/microsoft_graph.rb +++ b/lib/microsoft_graph.rb @@ -2,49 +2,29 @@ require 'httparty' class MicrosoftGraph BASE_URL = 'https://graph.microsoft.com/v1.0'.freeze - BETA_BASE_URL = 'https://graph.microsoft.com/beta'.freeze - # Documentation says that "common" can be used (instead of tenantID) to refresh access token - REFRESH_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/token'.freeze - def initialize(current_user, client_id, client_secret, scopes) + def initialize(current_user, client_id, client_secret, tenant_id, scopes) @current_user = current_user @uid = current_user.microsoft_user_id @token = current_user.microsoft_access_token @refresh_token = current_user.microsoft_refresh_token - @expires_at = current_user.microsoft_token_expires_at + @token_expires_at = current_user.microsoft_token_expires_at @client_id = client_id @client_secret = client_secret + @tenant_id = tenant_id @scopes = scopes end - def request(resource) - return false unless @token - - response = HTTParty.get( - "#{BASE_URL}/#{resource}", - headers: { - Authorization: "Bearer #{@token}" - } - ) - - if response.code != 200 - p '[Microsoft Graph API Error]' - p response.inspect - false - else - JSON.parse(response.body) + def create_teams_meeting(subject) + # Obtain new token if token is expired or will expire in less than 5 minutes + if 5.minutes.from_now.to_i > @token_expires_at.seconds + refresh_access_token end - end - - def create_teams_meeting(start_date_time, end_date_time, subject) - # check if token expired and obtain new access_token using refresh token response = HTTParty.post( - "#{BETA_BASE_URL}/me/onlineMeetings", + "#{BASE_URL}/me/onlineMeetings", body: { - startDateTime: start_date_time, - endDateTime: end_date_time, subject: subject, participants: { organizer: { @@ -55,22 +35,35 @@ class MicrosoftGraph } } } - }, + }.to_json, headers: { - Authorization: "Bearer #{@token}" + Authorization: "Bearer #{@token}", + 'Content-Type': 'application/json' } ) + raise StandardError, 'Authenticated user does not have a permission to create Teams Online Meeting' if response.code == 403 + if response.code != 201 - p '[Microsoft Graph API Error] Failed to create online meeting' - p response.inspect + Rails.logger.error('[Microsoft Graph Error]') + Rails.logger.error(response.inspect) + raise StandardError, "Failed to create teams meeting [#{response.code}]" else JSON.parse(response.body) end end - def refresh_token - response = HTTParty.post(REFRESH_TOKEN_URL, + private + + def refresh_token_url + "https://login.microsoftonline.com/#{@tenant_id}/oauth2/v2.0/token" + end + + def refresh_access_token + response = HTTParty.post(refresh_token_url, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, body: { client_id: @client_id, client_secret: @client_secret, @@ -79,5 +72,26 @@ class MicrosoftGraph scope: @scopes }) + if response.code != 200 + Rails.logger.error '[Microsoft Graph Error] Failed to obtain new access token using refresh token' + Rails.logger.error(response.inspect) + raise StandardError, 'Failed to obtain new access token' + end + + parsed_response = JSON.parse(response.body) + + new_access_token = parsed_response['access_token'] + new_refresh_token = parsed_response['refresh_token'] + token_expires_in = parsed_response['expires_in'] # For how long access token is valid (in seconds) + token_new_expiration_time = Time.now.to_i + token_expires_in + + @current_user.microsoft_access_token = new_access_token + @current_user.microsoft_refresh_token = new_refresh_token + @current_user.microsoft_token_expires_at = token_new_expiration_time + @current_user.save! + + @token = new_access_token + @refresh_token = new_refresh_token + @token_expires_at = token_new_expiration_time end end \ No newline at end of file -- 2.47.3 From 27fade2d09ba3114a6c7f53d08905b2fa40a5c3c Mon Sep 17 00:00:00 2001 From: Bilal Date: Tue, 18 Aug 2020 20:46:38 +0300 Subject: [PATCH 5/7] remove obsolete require --- app/models/broadcast.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/broadcast.rb b/app/models/broadcast.rb index 42df29b..d1f6943 100644 --- a/app/models/broadcast.rb +++ b/app/models/broadcast.rb @@ -1,5 +1,4 @@ class Broadcast < ApplicationRecord - require 'microsoft_graph' include PgSearch belongs_to :project -- 2.47.3 From f5628c47df6cf9c5294543720066823dce1e65c3 Mon Sep 17 00:00:00 2001 From: Bilal Date: Wed, 19 Aug 2020 13:05:16 +0300 Subject: [PATCH 6/7] allow selecting conference option --- app/controllers/broadcasts_controller.rb | 24 +---------- .../conference_meetings_controller.rb | 42 +++++++++++++++++++ app/helpers/broadcast_conferences_helper.rb | 17 ++++++++ app/views/broadcasts/_form.html.erb | 1 + app/views/broadcasts/show.html.erb | 4 +- config/locales/en.yml | 3 ++ config/locales/es.yml | 3 ++ config/routes.rb | 2 +- ...53_add_conference_details_to_broadcasts.rb | 6 +++ ...ft_teams_meeting_join_url_to_broadcasts.rb | 5 --- db/structure.sql | 3 +- lib/microsoft_graph.rb | 10 +++++ 12 files changed, 89 insertions(+), 31 deletions(-) create mode 100644 app/controllers/conference_meetings_controller.rb create mode 100644 app/helpers/broadcast_conferences_helper.rb create mode 100644 db/migrate/20200817233053_add_conference_details_to_broadcasts.rb delete mode 100644 db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index fa93af8..bc7c561 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -18,25 +18,6 @@ class BroadcastsController < ApplicationController def create @broadcast.attributes = broadcast_params - begin - graph_api = MicrosoftGraph.new( - current_user, - ENV['AZURE_CLIENT_ID'], - ENV['AZURE_CLIENT_SECRET'], - ENV['AZURE_TENANT_ID'], - ENV['AZURE_SCOPES'] - ) - - subject = "#{@broadcast.name} Online Meeting" - teams_meeting = graph_api.create_teams_meeting(subject) - join_url = teams_meeting['joinUrl'] - @broadcast.microsoft_teams_meeting_url = join_url if join_url.present? - rescue StandardError => e - @broadcast.errors[:base] << e.message - render :new - return - end - if @broadcast.save log_create_analytics redirect_to [@project, :broadcasts], notice: t(".notice") @@ -46,8 +27,7 @@ class BroadcastsController < ApplicationController end def show - # @conference_url = url_for [@broadcast.project, @broadcast, :zoom_meeting] - @conference_url = @broadcast.microsoft_teams_meeting_url + @conference_url = url_for [@broadcast.project, @broadcast, :conference_meeting] @recordings = @broadcast.broadcast_recordings.order_by_recent.paginate(page: params[:page]) @files = @broadcast.files.order("created_at DESC").paginate(page: params[:files_page]) render layout: 'application' @@ -93,7 +73,7 @@ class BroadcastsController < ApplicationController end def broadcast_params - params.require(:broadcast).permit(:name, :shoot_location_time_zone, files: []) + params.require(:broadcast).permit(:name, :shoot_location_time_zone, :conference_option, files: []) end def set_project diff --git a/app/controllers/conference_meetings_controller.rb b/app/controllers/conference_meetings_controller.rb new file mode 100644 index 0000000..2515ea7 --- /dev/null +++ b/app/controllers/conference_meetings_controller.rb @@ -0,0 +1,42 @@ +class ConferenceMeetingsController < ApplicationController + def show + authorize broadcast = Broadcast.find(params[:broadcast_id]) + case broadcast.conference_option + when 'zoom' + redirect_to broadcast.zoom_meeting_url + when 'ms_teams' + if broadcast.conference_join_url.nil? + begin + graph_api = MicrosoftGraph.new( + current_user, + ENV['AZURE_CLIENT_ID'], + ENV['AZURE_CLIENT_SECRET'], + ENV['AZURE_TENANT_ID'], + ENV['AZURE_SCOPES'] + ) + + subject = "#{broadcast.name} Online Meeting" + teams_meeting = graph_api.create_teams_meeting(subject) + join_url = teams_meeting['joinUrl'] + if join_url.present? + broadcast.conference_join_url = join_url + broadcast.save + else + raise StandardError, 'Failed to read teams meeting join URL' + end + rescue ActionController::InvalidAuthenticityToken => e + Rails.logger.error(e.message) + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'You are not authenticated via Microsoft, please authenticate and try again' + return + rescue StandardError => e + Rails.logger.error(e.message) + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'Failed to join conference' + return + end + end + redirect_to broadcast.conference_join_url + else + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'Unknown conference option' + end + end +end \ No newline at end of file diff --git a/app/helpers/broadcast_conferences_helper.rb b/app/helpers/broadcast_conferences_helper.rb new file mode 100644 index 0000000..05ab097 --- /dev/null +++ b/app/helpers/broadcast_conferences_helper.rb @@ -0,0 +1,17 @@ +module BroadcastConferencesHelper + def options_for_conference_select + [ + ['Zoom', 'zoom'], + ['MS Teams', 'ms_teams'] + ] + end + + def conference_option_name_from_key(key) + option = options_for_conference_select.find { |option| option[1] == key } + if option.present? + option.first + else + 'Unknown conference option' + end + end +end \ No newline at end of file diff --git a/app/views/broadcasts/_form.html.erb b/app/views/broadcasts/_form.html.erb index 2efc4bd..b60665d 100644 --- a/app/views/broadcasts/_form.html.erb +++ b/app/views/broadcasts/_form.html.erb @@ -2,6 +2,7 @@ <%= bootstrap_form_with model: model, local: true do |form| %> <%= form.text_field :name %> + <%= form.select :conference_option, options_for_conference_select, { label: t('.labels.conference_option') }, class: "form-control custom-select" %> <%= form.time_zone_select(:shoot_location_time_zone, nil, label: "Time zone of shoot location") %>
diff --git a/app/views/broadcasts/show.html.erb b/app/views/broadcasts/show.html.erb index ccd5fc2..c6a94c3 100644 --- a/app/views/broadcasts/show.html.erb +++ b/app/views/broadcasts/show.html.erb @@ -118,8 +118,8 @@

<% end %> -

If you want to join the ZOOM meeting dedicated to this broadcast, follow the link below.

- <%= link_to 'Video Conference', @conference_url, class: "btn btn-primary btn-block #{@conference_url.present? ? '' : 'disabled'}", target: '_blank' %> +

<%= "If you want to join the #{conference_option_name_from_key(@broadcast.conference_option)} meeting dedicated to this broadcast, follow the link below." %>

+ <%= link_to 'Video Conference', @conference_url, class: "btn btn-primary btn-block", target: '_blank' %>
params[:active_tab] == 'recordings') %>" id="recordings">
diff --git a/config/locales/en.yml b/config/locales/en.yml index 46f0db3..ae6cbb9 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -262,6 +262,9 @@ en: stream_multiple_cameras: Stream multiple cameras at one time update: reset_notice: The Share URL has been reset, and the previous URL will no longer work. Please click "Copy URL" and share it again with those who you want to have access to this live stream + form: + labels: + conference_option: Conference Option bulk_taggings: new_bulk_tag_modal: submit: Add diff --git a/config/locales/es.yml b/config/locales/es.yml index db3048b..530909f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -120,6 +120,9 @@ es: stream_multiple_cameras: Stream multiple cameras at one time update: reset_notice: The Share URL has been reset, and the previous URL will no longer work. Please click "Copy URL" and share it again with those who you want to have access to this live stream + form: + labels: + conference_option: Conference Option (ES) contract_templates: blank_contracts: create: diff --git a/config/routes.rb b/config/routes.rb index 5bdeac1..7dcfb5e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -101,7 +101,7 @@ Rails.application.routes.draw do member do delete :destroy_file end - # resource :zoom_meeting, only: [:show] + resource :conference_meeting, only: [:show] end resources :directories, except: [:index] do member do diff --git a/db/migrate/20200817233053_add_conference_details_to_broadcasts.rb b/db/migrate/20200817233053_add_conference_details_to_broadcasts.rb new file mode 100644 index 0000000..72a5d9e --- /dev/null +++ b/db/migrate/20200817233053_add_conference_details_to_broadcasts.rb @@ -0,0 +1,6 @@ +class AddConferenceDetailsToBroadcasts < ActiveRecord::Migration[6.0] + def change + add_column :broadcasts, :conference_option, :string + add_column :broadcasts, :conference_join_url, :string + end +end \ No newline at end of file diff --git a/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb b/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb deleted file mode 100644 index ddff8e9..0000000 --- a/db/migrate/20200817233053_add_microsoft_teams_meeting_join_url_to_broadcasts.rb +++ /dev/null @@ -1,5 +0,0 @@ -class AddMicrosoftTeamsMeetingJoinUrlToBroadcasts < ActiveRecord::Migration[6.0] - def change - add_column :broadcasts, :microsoft_teams_meeting_url, :string - end -end \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 1707992..866d2fa 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -563,7 +563,8 @@ CREATE TABLE public.broadcasts ( streamer_status integer DEFAULT 0, shoot_location_time_zone character varying DEFAULT 'UTC'::character varying, full_live_stream_playback_uid character varying, - microsoft_teams_meeting_url character varying + conference_option character varying, + conference_join_url character varying ); diff --git a/lib/microsoft_graph.rb b/lib/microsoft_graph.rb index 586eea6..27531cc 100644 --- a/lib/microsoft_graph.rb +++ b/lib/microsoft_graph.rb @@ -17,11 +17,21 @@ class MicrosoftGraph end def create_teams_meeting(subject) + if @refresh_token.nil? || @token_expires_at.nil? + raise ActionController::InvalidAuthenticityToken, 'Missing refresh token / token expiration' + return + end + # Obtain new token if token is expired or will expire in less than 5 minutes if 5.minutes.from_now.to_i > @token_expires_at.seconds refresh_access_token end + if @token.nil? + raise ActionController::InvalidAuthenticityToken, 'Missing access token' + return + end + response = HTTParty.post( "#{BASE_URL}/me/onlineMeetings", body: { -- 2.47.3 From 51478064837c0ad849f591330e406488a905e660 Mon Sep 17 00:00:00 2001 From: Bilal Date: Wed, 19 Aug 2020 15:47:01 +0300 Subject: [PATCH 7/7] add specs and use i18n --- app/controllers/broadcasts_controller.rb | 1 - .../conference_meetings_controller.rb | 9 ++- config/locales/en.yml | 6 ++ config/locales/es.yml | 8 ++ .../controllers/broadcasts_controller_spec.rb | 15 +++- .../conference_meetings_controller_spec.rb | 76 +++++++++++++++++++ .../zoom_meetings_controller_spec.rb | 30 -------- spec/factories/broadcasts.rb | 5 ++ .../features/user_managing_broadcasts_spec.rb | 7 ++ 9 files changed, 120 insertions(+), 37 deletions(-) create mode 100644 spec/controllers/conference_meetings_controller_spec.rb delete mode 100644 spec/controllers/zoom_meetings_controller_spec.rb diff --git a/app/controllers/broadcasts_controller.rb b/app/controllers/broadcasts_controller.rb index bc7c561..af38f6d 100644 --- a/app/controllers/broadcasts_controller.rb +++ b/app/controllers/broadcasts_controller.rb @@ -1,5 +1,4 @@ class BroadcastsController < ApplicationController - require 'microsoft_graph' layout "project" before_action :set_project diff --git a/app/controllers/conference_meetings_controller.rb b/app/controllers/conference_meetings_controller.rb index 2515ea7..40b85dc 100644 --- a/app/controllers/conference_meetings_controller.rb +++ b/app/controllers/conference_meetings_controller.rb @@ -1,4 +1,6 @@ class ConferenceMeetingsController < ApplicationController + require 'microsoft_graph' + def show authorize broadcast = Broadcast.find(params[:broadcast_id]) case broadcast.conference_option @@ -18,6 +20,7 @@ class ConferenceMeetingsController < ApplicationController subject = "#{broadcast.name} Online Meeting" teams_meeting = graph_api.create_teams_meeting(subject) join_url = teams_meeting['joinUrl'] + if join_url.present? broadcast.conference_join_url = join_url broadcast.save @@ -26,17 +29,17 @@ class ConferenceMeetingsController < ApplicationController end rescue ActionController::InvalidAuthenticityToken => e Rails.logger.error(e.message) - redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'You are not authenticated via Microsoft, please authenticate and try again' + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.not_authenticated') return rescue StandardError => e Rails.logger.error(e.message) - redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'Failed to join conference' + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.failed_to_join') return end end redirect_to broadcast.conference_join_url else - redirect_to project_broadcast_url(broadcast.project, broadcast), alert: 'Unknown conference option' + redirect_to project_broadcast_url(broadcast.project, broadcast), alert: t('.alerts.unknown_conference_option') end end end \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index ae6cbb9..af6396a 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1557,3 +1557,9 @@ en: edit: Edit report: Report generating: Generating... + conference_meetings: + show: + alerts: + not_authenticated: You are not authenticated via Microsoft, please authenticate and try again + failed_to_join: Failed to join conference + unknown_conference_option: Unknown conference option diff --git a/config/locales/es.yml b/config/locales/es.yml index 530909f..9f9c267 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -633,3 +633,11 @@ es: production_elements_logs: Production Elements Logs, and more (ES) reduces_labor_cost: Reduces labor costs (ES) simplifies_cue_sheets: Simplifies Music Cue Sheets, Graphic Cue Sheets (ES) + conference_meetings: + show: + alert: + not_authenticated: "" + alerts: + not_authenticated: You are not authenticated via Microsoft, please authenticate and try again (ES) + failed_to_join: Failed to join conference (ES) + unknown_conference_option: Unknown conference option (ES) diff --git a/spec/controllers/broadcasts_controller_spec.rb b/spec/controllers/broadcasts_controller_spec.rb index ed37ad2..072ad00 100644 --- a/spec/controllers/broadcasts_controller_spec.rb +++ b/spec/controllers/broadcasts_controller_spec.rb @@ -131,16 +131,25 @@ RSpec.describe BroadcastsController, type: :controller do expect(response.body).to have_xpath "//input[@readonly][@value='#{broadcast_url(broadcast.token)}']" end - it "displays zoom meeting button" do + it "displays zoom meeting button for zoom conference option" do get :show, params: { project_id: project.id, id: broadcast.id } - expect(response.body).to have_link("Video Conference", href: project_broadcast_zoom_meeting_url(project, broadcast)) + expect(response.body).to have_content 'Zoom' + expect(response.body).to have_link("Video Conference", href: project_broadcast_conference_meeting_url(project, broadcast)) + end + + it "displays microsoft teams meeting button for MS Teams conference option" do + ms_teams_broadcast = create(:broadcast, :ms_teams_conference, project: project ) + get :show, params: { project_id: project.id, id: ms_teams_broadcast.id } + + expect(response.body).to have_content 'MS Teams' + expect(response.body).to have_link 'Video Conference', href: project_broadcast_conference_meeting_url(project, ms_teams_broadcast) end it "assigns required variables" do get :show, params: { project_id: project.id, id: broadcast.id } - expect(assigns(:conference_url)).to eq project_broadcast_zoom_meeting_url(project, broadcast) + expect(assigns(:conference_url)).to eq project_broadcast_conference_meeting_url(project, broadcast) expect(assigns(:broadcast)).to eq broadcast end diff --git a/spec/controllers/conference_meetings_controller_spec.rb b/spec/controllers/conference_meetings_controller_spec.rb new file mode 100644 index 0000000..9b9167a --- /dev/null +++ b/spec/controllers/conference_meetings_controller_spec.rb @@ -0,0 +1,76 @@ +require 'rails_helper' + +RSpec.describe ConferenceMeetingsController, type: :controller do + let(:user) { create(:user) } + let(:account) { user.primary_account } + let(:project) { create(:project, account: user.primary_account) } + let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } + let(:ms_teams_broadcast) { create(:broadcast, :ms_teams_conference, project: project) } + let(:unknown_option_broadcast) { create(:broadcast, project: project, conference_option: 'google') } + + let(:meeting_start_url) { "http://meeting_start_url" } + let(:meeting_hash) { HashWithIndifferentAccess.new(start_url: meeting_start_url) } + let(:user_create_response) { {"id" => "new_host_id"} } + let(:roles_assign_response) { {"ids" => ["new_host_id"]} } + let(:roles_list_response) { {"roles" => [{"name" => "directme-host"}]} } + + before :each do + allow_any_instance_of(ZoomGateway).to receive(:find_meeting).and_return(meeting_hash) + allow_any_instance_of(ZoomGateway).to receive(:create_meeting).and_return("meeting_id") + allow_any_instance_of(ZoomGateway).to receive(:create_host).and_return("host_id") + allow(MuxLiveStream).to receive(:new).and_return OpenStruct.new(id: 'id', key: 'key', playback_id: 'playback_id') + end + + describe "#show" do + before { sign_in user } + + it "redirects to meeting start url with Zoom conference option" do + get :show, params: { project_id: project.id, broadcast_id: broadcast.id } + expect(response).to redirect_to(meeting_start_url) + end + + it "redirects to the broadcast show page with alert if user is not authenticated via microsoft and tries to create MS Teams meeting" do + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, ms_teams_broadcast) + expect(flash.alert).to eq not_authenticated_alert + end + + it "redirects to the broadcast show page with alert if user is authenticated via microsoft and tries to create MS Teams meeting but Graph API fails to create meeting" do + allow_any_instance_of(MicrosoftGraph).to receive(:create_teams_meeting).and_return(nil) + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, ms_teams_broadcast) + expect(flash.alert).to eq failed_to_join_alert + end + + it "redirects to the broadcast show page with alert if conference option is not reckognized" do + get :show, params: { project_id: project.id, broadcast_id: unknown_option_broadcast.id } + expect(response).to redirect_to project_broadcast_path(project, unknown_option_broadcast) + expect(flash.alert).to eq unknown_conference_option_alert + end + + it "redirects to meeting start url with MS Teams conference option" do + new_ms_teams_meeting = JSON.parse({ + joinUrl: meeting_start_url + }.to_json) + + allow_any_instance_of(MicrosoftGraph).to receive(:create_teams_meeting).and_return(new_ms_teams_meeting) + + get :show, params: { project_id: project.id, broadcast_id: ms_teams_broadcast.id } + expect(response).to redirect_to(meeting_start_url) + end + end + + private + + def not_authenticated_alert + t 'conference_meetings.show.alerts.not_authenticated' + end + + def failed_to_join_alert + t 'conference_meetings.show.alerts.failed_to_join' + end + + def unknown_conference_option_alert + t 'conference_meetings.show.alerts.unknown_conference_option' + end +end diff --git a/spec/controllers/zoom_meetings_controller_spec.rb b/spec/controllers/zoom_meetings_controller_spec.rb deleted file mode 100644 index ca1ab04..0000000 --- a/spec/controllers/zoom_meetings_controller_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'rails_helper' - -RSpec.describe ZoomMeetingsController, type: :controller do - let(:user) { create(:user) } - let(:account) { user.primary_account } - let(:project) { create(:project, account: user.primary_account) } - let(:broadcast) { create(:broadcast, name: "Broadcast", project: project) } - - let(:meeting_start_url) { "http://meeting_start_url" } - let(:meeting_hash) { HashWithIndifferentAccess.new(start_url: meeting_start_url) } - let(:user_create_response) { {"id" => "new_host_id"} } - let(:roles_assign_response) { {"ids" => ["new_host_id"]} } - let(:roles_list_response) { {"roles" => [{"name" => "directme-host"}]} } - - before :each do - allow_any_instance_of(ZoomGateway).to receive(:find_meeting).and_return(meeting_hash) - allow_any_instance_of(ZoomGateway).to receive(:create_meeting).and_return("meeting_id") - allow_any_instance_of(ZoomGateway).to receive(:create_host).and_return("host_id") - allow(MuxLiveStream).to receive(:new).and_return OpenStruct.new(id: 'id', key: 'key', playback_id: 'playback_id') - end - - describe "#show" do - before { sign_in user } - - it "redirects to meeting start url" do - get :show, params: { project_id: project.id, broadcast_id: broadcast.id } - expect(response).to redirect_to(meeting_start_url) - end - end -end diff --git a/spec/factories/broadcasts.rb b/spec/factories/broadcasts.rb index 4148242..33da96c 100644 --- a/spec/factories/broadcasts.rb +++ b/spec/factories/broadcasts.rb @@ -2,11 +2,16 @@ FactoryBot.define do factory :broadcast do association :project name "My Live Stream" + conference_option "zoom" transient do skip_create_callback false end + trait :ms_teams_conference do + conference_option "ms_teams" + end + trait :with_stream do stream_uid "mux_stream" stream_key "mux_key" diff --git a/spec/features/user_managing_broadcasts_spec.rb b/spec/features/user_managing_broadcasts_spec.rb index b4acf4e..6e2fa6a 100644 --- a/spec/features/user_managing_broadcasts_spec.rb +++ b/spec/features/user_managing_broadcasts_spec.rb @@ -24,6 +24,7 @@ feature 'User managing broadcasts' do by 'filling out the form' do fill_in broadcast_name_field, with: 'My Broadcast' + select_conference_option('Zoom') select_time_zone("New Delhi") end @@ -267,6 +268,12 @@ feature 'User managing broadcasts' do end end + def select_conference_option(value) + if value.present? + select value, from: "broadcast[conference_option]" + end + end + def click_checkboxes all('input[type="checkbox"]')[0].click all('input[type="checkbox"]')[1].click -- 2.47.3