From cb5fd0c2c0e99e8fd120c4c96e9088f605faf98e Mon Sep 17 00:00:00 2001 From: Martin Edenhofer Date: Wed, 25 May 2016 09:19:45 +0200 Subject: [PATCH] Reworked admin maintenance area (needed for restarting screen). --- .../app/controllers/_settings/area.coffee | 17 +- .../javascripts/app/controllers/login.coffee | 20 +- .../app/controllers/maintenance.coffee | 65 ++- .../javascripts/app/controllers/signup.coffee | 13 +- .../widget/app_config_update.coffee | 12 + .../app/controllers/widget/app_version.coffee | 33 -- .../app/controllers/widget/maintenance.coffee | 77 +++- .../javascripts/app/models/setting.coffee | 2 +- .../app/views/generic/login_preview.jst.eco | 43 ++ .../javascripts/app/views/login.jst.eco | 37 +- .../javascripts/app/views/maintenance.jst.eco | 68 ++- .../app/views/settings/logo.jst.eco | 36 +- app/controllers/application_controller.rb | 35 +- app/controllers/sessions_controller.rb | 17 +- app/controllers/settings_controller.rb | 2 +- app/models/setting.rb | 20 +- .../20160550000001_update_maintenance.rb | 35 ++ db/seeds.rb | 30 ++ lib/app_version.rb | 6 +- lib/sessions.rb | 21 +- lib/sessions/event/base.rb | 45 ++ lib/sessions/event/chat_agent_state.rb | 2 +- lib/sessions/event/chat_base.rb | 45 -- lib/sessions/event/chat_session_start.rb | 2 +- lib/sessions/event/chat_status_agent.rb | 2 +- lib/sessions/event/maintenance.rb | 22 + script/build/test_slice_tests.sh | 9 +- test/browser/maintenance_app_version_test.rb | 36 -- test/browser/maintenance_message_test.rb | 174 -------- test/browser/maintenance_test.rb | 413 ++++++++++++++++++ test/browser_test_helper.rb | 93 +++- 31 files changed, 1025 insertions(+), 407 deletions(-) create mode 100644 app/assets/javascripts/app/controllers/widget/app_config_update.coffee delete mode 100644 app/assets/javascripts/app/controllers/widget/app_version.coffee create mode 100644 app/assets/javascripts/app/views/generic/login_preview.jst.eco create mode 100644 db/migrate/20160550000001_update_maintenance.rb create mode 100644 lib/sessions/event/maintenance.rb delete mode 100644 test/browser/maintenance_app_version_test.rb delete mode 100644 test/browser/maintenance_message_test.rb create mode 100644 test/browser/maintenance_test.rb diff --git a/app/assets/javascripts/app/controllers/_settings/area.coffee b/app/assets/javascripts/app/controllers/_settings/area.coffee index d36cc13d4..6861e027a 100644 --- a/app/assets/javascripts/app/controllers/_settings/area.coffee +++ b/app/assets/javascripts/app/controllers/_settings/area.coffee @@ -132,10 +132,14 @@ class App.SettingsAreaLogo extends App.Controller @render() render: -> - @html App.view('settings/logo')( + localElement = $(App.view('settings/logo')( setting: @setting + )) + localElement.find('.js-loginPreview').html( App.view('generic/login_preview')( logoUrl: @logoUrl() - ) + logoChange: true + )) + @html localElement onLogoPick: (event) => reader = new FileReader() @@ -146,8 +150,7 @@ class App.SettingsAreaLogo extends App.Controller file = event.target.files[0] # if no file is given, about in file upload was used - if !file - return + return if !file maxSiteInMb = 8 if file.size && file.size > 1024 * 1024 * maxSiteInMb @@ -189,9 +192,9 @@ class App.SettingsAreaLogo extends App.Controller msg: App.i18n.translateContent('Update successful!') timeout: 2000 } - - for key, value of data.settings - App.Config.set( key, value ) + for setting in data.settings + value = App.Setting.get(setting.name) + App.Config.set(name, value) else App.Event.trigger 'notify', { type: 'error' diff --git a/app/assets/javascripts/app/controllers/login.coffee b/app/assets/javascripts/app/controllers/login.coffee index 64f952179..106a12297 100644 --- a/app/assets/javascripts/app/controllers/login.coffee +++ b/app/assets/javascripts/app/controllers/login.coffee @@ -21,6 +21,20 @@ class Index extends App.ControllerContent @render() @navupdate '#login' + # observe config changes related to login page + @bind('config_update_local', (data) => + return if data.name != 'maintenance_mode' && + data.name != 'maintenance_login' && + data.name != 'maintenance_login_message' && + data.name != 'user_lost_password' && + data.name != 'user_create_account' && + data.name != 'product_name' && + data.name != 'product_logo' && + data.name != 'fqdn' + @render() + 'rerender' + ) + render: (data = {}) -> auth_provider_all = { facebook: { @@ -100,11 +114,15 @@ class Index extends App.ControllerContent @navigate '#/' error: (xhr, statusText, error) => + detailsRaw = xhr.responseText + details = {} + if !_.isEmpty(detailsRaw) + details = JSON.parse(detailsRaw) # add notify @notify type: 'error' - msg: App.i18n.translateContent('Wrong Username and Password combination.') + msg: App.i18n.translateContent(details.error || 'Wrong Username and Password combination.') removeAll: true # rerender login page diff --git a/app/assets/javascripts/app/controllers/maintenance.coffee b/app/assets/javascripts/app/controllers/maintenance.coffee index 3cee0689a..8a556414a 100644 --- a/app/assets/javascripts/app/controllers/maintenance.coffee +++ b/app/assets/javascripts/app/controllers/maintenance.coffee @@ -1,6 +1,13 @@ class Index extends App.ControllerContent events: - 'submit form': 'sendMessage' + 'change .js-modeSetting input': 'setMode' + 'change .js-loginSetting input': 'setLogin' + 'blur .js-Login': 'updateMessage' + 'submit .js-Message': 'sendMessage' + + elements: + '.js-modeSetting input': 'modeSetting' + '.js-loginSetting input': 'loginSetting' constructor: -> super @@ -10,20 +17,56 @@ class Index extends App.ControllerContent @title 'Maintenance', true - @render() + @subscribeId = App.Setting.subscribe(@render, initFetch: true, clear: false) - render: -> - @html App.view('maintenance')() + release: => + App.Setting.unsubscribe(@subscribeId) + + render: => + localElement = $(App.view('maintenance')()) + localElement.find('.js-loginPreview').html( App.view('generic/login_preview')( + logoUrl: @logoUrl() + )) + + localElement.find('.js-textarea').ce({ + mode: 'richtext' + multiline: true + maxlength: 20000 + }) + + @html localElement + + setMode: (e) => + value = @modeSetting.prop('checked') + return if value && !confirm('Sure?') + App.Setting.set('maintenance_mode', value) + App.WebSocket.send( + event:'maintenance' + data: + type: 'mode' + on: value + ) + + setLogin: (e) => + value = @loginSetting.prop('checked') + App.Setting.set('maintenance_login', value) + + updateMessage: (e) => + e.preventDefault() + params = @formParam(e.target) + App.Setting.set('maintenance_login_message', params.message) + @notify + type: 'success' + msg: App.i18n.translateContent('Update successful!') + removeAll: true sendMessage: (e) -> e.preventDefault() params = @formParam(e.target) - App.Event.trigger( - 'ws:send' - event: 'broadcast' - data: - event: 'session:maintenance' - data: params + params.type = 'message' + App.WebSocket.send( + event:'maintenance' + data: params ) @notify type: 'success' @@ -31,4 +74,4 @@ class Index extends App.ControllerContent removeAll: true @render() -App.Config.set( 'Maintenance', { prio: 3600, name: 'Maintenance', parent: '#system', target: '#system/maintenance', controller: Index, role: ['Admin'] }, 'NavBarAdmin' ) \ No newline at end of file +App.Config.set('Maintenance', { prio: 3600, name: 'Maintenance', parent: '#system', target: '#system/maintenance', controller: Index, role: ['Admin'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/signup.coffee b/app/assets/javascripts/app/controllers/signup.coffee index 1253c5616..e5a5a956c 100644 --- a/app/assets/javascripts/app/controllers/signup.coffee +++ b/app/assets/javascripts/app/controllers/signup.coffee @@ -81,18 +81,21 @@ class Index extends App.ControllerContent # add notify @notify type: 'success' - msg: 'Thanks for joining. Email sent to "' + @params.email + '". Please verify your email address.' + msg: App.i18n.translateContent('Thanks for joining. Email sent to "%s". Please verify your email address.', @params.email) removeAll: true # redirect to # @navigate '#' error: (xhr, statusText, error) => + detailsRaw = xhr.responseText + details = {} + if !_.isEmpty(detailsRaw) + details = JSON.parse(detailsRaw) - # add notify @notify - type: 'warning' - msg: 'Wrong Username and Password combination.' + type: 'error' + msg: App.i18n.translateContent(details.error || 'Wrong Username and Password combination.') removeAll: true -App.Config.set( 'signup', Index, 'Routes' ) +App.Config.set('signup', Index, 'Routes') diff --git a/app/assets/javascripts/app/controllers/widget/app_config_update.coffee b/app/assets/javascripts/app/controllers/widget/app_config_update.coffee new file mode 100644 index 000000000..5a0f48ed3 --- /dev/null +++ b/app/assets/javascripts/app/controllers/widget/app_config_update.coffee @@ -0,0 +1,12 @@ +class Widget extends App.Controller + constructor: -> + super + + App.Event.bind( + 'config_update' + (data) -> + App.Config.set(data.name, data.value) + App.Event.trigger('config_update_local', data) + ) + +App.Config.set('app_config_update', Widget, 'Widgets') diff --git a/app/assets/javascripts/app/controllers/widget/app_version.coffee b/app/assets/javascripts/app/controllers/widget/app_version.coffee deleted file mode 100644 index b35d0c5ea..000000000 --- a/app/assets/javascripts/app/controllers/widget/app_version.coffee +++ /dev/null @@ -1,33 +0,0 @@ -class Widget extends App.Controller - constructor: -> - super - - App.Event.bind( - 'app_version' - (data) => - @render(data) - 'app_version' - ) - - render: (data) => - return if @message - return if @appVersion is data.app_version - if !@appVersion - @appVersion = data.app_version - return - @appVersion = data.app_version - localAppVersion = @appVersion.split(':') - return if localAppVersion[1] isnt 'true' - message = => - @message = new App.SessionMessage( - head: 'New Version' - message: 'A new version of Zammad is available, please reload your browser.' - keyboard: false - backdrop: true - buttonClose: false - buttonSubmit: 'Continue session' - forceReload: true - ) - @delay(message, 2000) - -App.Config.set('app_version', Widget, 'Widgets') diff --git a/app/assets/javascripts/app/controllers/widget/maintenance.coffee b/app/assets/javascripts/app/controllers/widget/maintenance.coffee index c9ffbd97f..f6a0dd057 100644 --- a/app/assets/javascripts/app/controllers/widget/maintenance.coffee +++ b/app/assets/javascripts/app/controllers/widget/maintenance.coffee @@ -2,11 +2,19 @@ class Widget extends App.Controller constructor: -> super - # bind on event to show message App.Event.bind( - 'session:maintenance' + 'maintenance' (data) => - @showMessage(data) + if data.type is 'message' + @showMessage(data) + if data.type is 'mode' + @maintanaceMode(data) + if data.type is 'app_version' + @maintanaceAppVersion(data) + if data.type is 'config_changed' + @maintanaceConfigChanged(data) + if data.type is 'restart' + @maintanaceRestart(data) 'maintenance' ) @@ -17,13 +25,10 @@ class Widget extends App.Controller else button = 'Close' - # convert to html and linkify - message.message = App.Utils.textCleanup(message.message) - message.message = App.Utils.text2html(message.message) - new App.SessionMessage( head: message.head contentInline: message.message + small: true keyboard: true backdrop: true buttonClose: true @@ -31,4 +36,62 @@ class Widget extends App.Controller forceReload: message.reload ) + maintanaceMode: (data = {}) => + return if data.on isnt true + return if !@authenticate(true) + @navigate '#logout' + + #App.Event.trigger('maintenance', {type:'restart'}) + maintanaceRestart: (data) => + return if @messageRestart + @messageRestart = new App.SessionMessage( + head: 'Restarting...' + message: 'Zammad is restarting... waiting...' + keyboard: false + backdrop: false + buttonClose: false + buttonSubmit: false + small: true + forceReload: true + ) + + # disconnect + + # try if backend is reachable again + + # reload app + + maintanaceConfigChanged: (data) => + return if @messageConfigChanged + @messageConfigChanged = new App.SessionMessage( + head: 'Config has changed' + message: 'The configuration of Zammad has changed, please reload your browser.' + keyboard: false + backdrop: true + buttonClose: false + buttonSubmit: 'Continue session' + forceReload: true + ) + + maintanaceAppVersion: (data) => + return if @messageAppVersion + return if @appVersion is data.app_version + if !@appVersion + @appVersion = data.app_version + return + @appVersion = data.app_version + localAppVersion = @appVersion.split(':') + return if localAppVersion[1] isnt 'true' + message = => + @messageAppVersion = new App.SessionMessage( + head: 'New Version' + message: 'A new version of Zammad is available, please reload your browser.' + keyboard: false + backdrop: true + buttonClose: false + buttonSubmit: 'Continue session' + forceReload: true + ) + @delay(message, 2000) + App.Config.set('maintenance', Widget, 'Widgets') diff --git a/app/assets/javascripts/app/models/setting.coffee b/app/assets/javascripts/app/models/setting.coffee index 321d7bccb..8e3a79d36 100644 --- a/app/assets/javascripts/app/models/setting.coffee +++ b/app/assets/javascripts/app/models/setting.coffee @@ -27,8 +27,8 @@ class App.Setting extends App.Model msg: App.i18n.translateContent(details.error_human || details.error || 'Unable to update object!') timeout: 2000 } - setting.save(options) App.Config.set(name, value) + setting.save(options) @preferencesPost: (setting) -> return if !setting.preferences diff --git a/app/assets/javascripts/app/views/generic/login_preview.jst.eco b/app/assets/javascripts/app/views/generic/login_preview.jst.eco new file mode 100644 index 000000000..e985dd4a3 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/login_preview.jst.eco @@ -0,0 +1,43 @@ +
+ + <% if @C('maintenance_mode'): %> +
<%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %>
+ <% end %> + + <% if !@logoChange || @C('maintenance_login'): %> +
+ +
+ <% end %> + +
+ + <% if @logoChange: %> + +
<%- @T('Your Logo') %>
+
+
<%- @T('Change') %>
+
+ <% else: %> + + <% end %> + +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
+
+
diff --git a/app/assets/javascripts/app/views/login.jst.eco b/app/assets/javascripts/app/views/login.jst.eco index d562d80cd..ff543fe8a 100644 --- a/app/assets/javascripts/app/views/login.jst.eco +++ b/app/assets/javascripts/app/views/login.jst.eco @@ -1,56 +1,63 @@
-

<%- @T( 'Login with %s', @C( 'fqdn' ) ) %>

+

<%- @T('Login with %s', @C('fqdn')) %>

+ + <% if @C('maintenance_mode'): %> +
<%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %>
+ <% end %> + <% if @C('maintenance_login') && @C('maintenance_login_message'): %> +
<%- @C('maintenance_login_message') %>
+ <% end %>
- +
- +
- +
- + <% if @C('user_lost_password'): %> - <%- @T( 'Forgot password?' ) %> + <%- @T('Forgot password?') %> <% end %>
- <% if !_.isEmpty( @auth_providers ): %> + <% if !_.isEmpty(@auth_providers): %>
- <%- @T( 'or sign in using' ) %> + <%- @T('or sign in using') %>
@@ -59,23 +66,23 @@

- <%- @T( "You're already registered with your email adress if you've been in touch with our support team.") %>
+ <%- @T("You're already registered with your email adress if you've been in touch with our support team.") %>
<% if @C('user_lost_password'): %> - <%- @T( "You can request your password") %> <%- @T( "here") %>. + <%- @T('You can request your password') %> <%- @T('here') %>. <% end %>

<% if @C('user_create_account'): %>

- <%- @T( 'Register as a new customer' ) %> + <%- @T('Register as a new customer') %>

<% end %>
<%- @Icon('logo') %> - <%- @T("Powered by") %> + <%- @T('Powered by') %> <%- @Icon('logotype', 'logotype') %>
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/maintenance.jst.eco b/app/assets/javascripts/app/views/maintenance.jst.eco index d9f083ca6..94ee30853 100644 --- a/app/assets/javascripts/app/views/maintenance.jst.eco +++ b/app/assets/javascripts/app/views/maintenance.jst.eco @@ -1,30 +1,58 @@
- -
- -
- + +
+
+
+ checked<% end %>> +
+

<%- @T('Mode') %>

-
- -
- +

⚠ <%- @T('Enable or disable the maintenance mode of Zammad. If enabled, _all non-administrators get logged out_ and _only administrators can start a new session_.') %>

+
+ +
+
+
+ checked<% end %>> +
+

@<%- @T('Login') %>

-
- -
- - +

<%- @T('Put a message on the login page. To change it, click on the text area below and change it inline.') %>

+
+
+ +
+

<%- @T('Message') %>

+

<%- @T('Send a message to all logged in users.') %>

+
+
+ +
+ +
+
+
+ +
+
+
+
+
+ +
+ +
+
\ No newline at end of file diff --git a/app/assets/javascripts/app/views/settings/logo.jst.eco b/app/assets/javascripts/app/views/settings/logo.jst.eco index 3868e12f3..767769243 100644 --- a/app/assets/javascripts/app/views/settings/logo.jst.eco +++ b/app/assets/javascripts/app/views/settings/logo.jst.eco @@ -1,36 +1,8 @@
-

<%- @T( @setting.title ) %>

-

<%- @T( @setting.description ) %>

- - - +

<%- @T(@setting.title) %>

+

<%- @T(@setting.description) %>

+
- +
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 37a12b9d3..d8f5fd348 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -243,6 +243,12 @@ class ApplicationController < ActionController::Base # check sso based authentication sso_userdata = User.sso(params) if sso_userdata + if check_maintenance_only(sso_userdata) + return { + auth: false, + message: 'Maintenance mode enabled!', + } + end session[:persistent] = true return { auth: true @@ -254,6 +260,12 @@ class ApplicationController < ActionController::Base logger.debug "http basic auth check '#{username}'" userdata = User.authenticate(username, password) next if !userdata + if check_maintenance_only(userdata) + return { + auth: false, + message: 'Maintenance mode enabled!', + } + end current_user_set(userdata) user_device_log(userdata, 'basic_auth') logger.debug "http basic auth for '#{userdata.login}'" @@ -271,6 +283,12 @@ class ApplicationController < ActionController::Base name: token, ) next if !userdata + if check_maintenance_only(userdata) + return { + auth: false, + message: 'Maintenance mode enabled!', + } + end current_user_set(userdata) user_device_log(userdata, 'token_auth') logger.debug "token auth for '#{userdata.login}'" @@ -345,7 +363,7 @@ class ApplicationController < ActionController::Base # config config = {} - Setting.select('name').where(frontend: true ).each { |setting| + Setting.select('name').where(frontend: true).each { |setting| config[setting.name] = Setting.get(setting.name) } @@ -480,4 +498,19 @@ class ApplicationController < ActionController::Base end data end + + # check maintenance mode + def check_maintenance_only(user) + return false if Setting.get('maintenance_mode') != true + return false if user.role?('Admin') + Rails.logger.info "Maintenance mode enabled, denied login for user #{user.login}, it's no admin user." + true + end + + def check_maintenance(user) + return false if !check_maintenance_only(user) + render json: { error: 'Maintenance mode enabled!' }, status: :unauthorized + true + end + end diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index bd2e2125f..a5ae7695e 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -11,9 +11,12 @@ class SessionsController < ApplicationController # authenticate user user = User.authenticate(params[:username], params[:password]) + # check maintenance mode + return if check_maintenance(user) + # auth failed if !user - render json: { error: 'login failed' }, status: :unauthorized + render json: { error: 'Wrong Username and Password combination.' }, status: :unauthorized return end @@ -144,6 +147,12 @@ class SessionsController < ApplicationController authorization = Authorization.create_from_hash(auth, current_user) end + # check maintenance mode + if check_maintenance_only(authorization.user) + redirect_to '/#' + return + end + # set current session user current_user_set(authorization.user) @@ -167,6 +176,12 @@ class SessionsController < ApplicationController # Log the authorizing user in. if user + # check maintenance mode + if check_maintenance_only(user) + redirect_to '/#' + return + end + # set current session user current_user_set(user) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index 4ea4557fc..fb0b021b2 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -70,7 +70,7 @@ class SettingsController < ApplicationController file = StaticAssets.data_url_attributes(params[:logo_resize]) # store image 1:1 - setting.state = StaticAssets.store( file[:content], file[:mime_type] ) + setting.state = StaticAssets.store(file[:content], file[:mime_type]) setting.save end diff --git a/app/models/setting.rb b/app/models/setting.rb index 63c520ccc..3e73a1844 100644 --- a/app/models/setting.rb +++ b/app/models/setting.rb @@ -5,8 +5,8 @@ class Setting < ApplicationModel store :state_current store :state_initial store :preferences - before_create :state_check, :set_initial - before_update :state_check + before_create :state_check, :set_initial, :check_broadcast + before_update :state_check, :check_broadcast after_create :reset_cache after_update :reset_cache after_destroy :reset_cache @@ -168,4 +168,20 @@ reload config settings return if state && state.respond_to?('has_key?') && state.key?(:value) self.state_current = { value: state } end + + # notify clients about public config changes + def check_broadcast + return if frontend != true + value = state_current + if state_current.key?(:value) + value = state_current[:value] + end + Sessions.broadcast( + { + event: 'config_update', + data: { name: name, value: value } + }, + 'public' + ) + end end diff --git a/db/migrate/20160550000001_update_maintenance.rb b/db/migrate/20160550000001_update_maintenance.rb new file mode 100644 index 000000000..5b85401a8 --- /dev/null +++ b/db/migrate/20160550000001_update_maintenance.rb @@ -0,0 +1,35 @@ +class UpdateMaintenance < ActiveRecord::Migration + def up + # can be deleted later, db/seeds.rb already updated + Setting.create_if_not_exists( + title: 'Maintenance Mode', + name: 'maintenance_mode', + area: 'Core::WebApp', + description: 'Enable or disable the maintenance mode of Zammad. If enabled, all non-administrators get logged out and only administrators can start a new session.', + options: {}, + state: false, + preferences: {}, + frontend: true + ) + Setting.create_if_not_exists( + title: 'Maintenance Login', + name: 'maintenance_login', + area: 'Core::WebApp', + description: 'Put a message on the login page. To change it, click on the text area below and change it inline.', + options: {}, + state: false, + preferences: {}, + frontend: true + ) + Setting.create_if_not_exists( + title: 'Maintenance Login', + name: 'maintenance_login_message', + area: 'Core::WebApp', + description: 'Message for login page.', + options: {}, + state: 'Something about to share. Click here to change.', + preferences: {}, + frontend: true + ) + end +end diff --git a/db/seeds.rb b/db/seeds.rb index 0dde55f88..a056e67c5 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -26,6 +26,36 @@ Setting.create_if_not_exists( preferences: { online_service_disable: true }, frontend: false ) +Setting.create_if_not_exists( + title: 'Maintenance Mode', + name: 'maintenance_mode', + area: 'Core::WebApp', + description: 'Enable or disable the maintenance mode of Zammad. If enabled, all non-administrators get logged out and only administrators can start a new session.', + options: {}, + state: false, + preferences: {}, + frontend: true +) +Setting.create_if_not_exists( + title: 'Maintenance Login', + name: 'maintenance_login', + area: 'Core::WebApp', + description: 'Put a message on the login page. To change it, click on the text area below and change it inline.', + options: {}, + state: false, + preferences: {}, + frontend: true +) +Setting.create_if_not_exists( + title: 'Maintenance Login', + name: 'maintenance_login_message', + area: 'Core::WebApp', + description: 'Message for login page.', + options: {}, + state: 'Something about to share. Click here to change.', + preferences: {}, + frontend: true +) Setting.create_if_not_exists( title: 'Developer System', name: 'developer_mode', diff --git a/lib/app_version.rb b/lib/app_version.rb index fcb40d45a..978c85272 100644 --- a/lib/app_version.rb +++ b/lib/app_version.rb @@ -44,8 +44,9 @@ get event data returnes { - event: 'app_version' + event: 'maintenance' data: { + type: 'app_version', app_version: app_version, } } @@ -54,8 +55,9 @@ returnes def self.event_data { - event: 'app_version', + event: 'maintenance', data: { + type: 'app_version', app_version: get, } } diff --git a/lib/sessions.rb b/lib/sessions.rb index b175bf781..5956903de 100644 --- a/lib/sessions.rb +++ b/lib/sessions.rb @@ -353,17 +353,32 @@ returns true|false +broadcase also to not authenticated client + + Sessions.broadcast(data, 'public') + +broadcase also not to sender + + Sessions.broadcast(data, 'public', sender_user_id) + =end - def self.broadcast(data) + def self.broadcast(data, recipient = 'autenticated', sender_user_id = nil) # list all current clients client_list = sessions client_list.each {|client_id| session = Sessions.get(client_id) next if !session - next if !session[:user] - next if !session[:user]['id'] + + if recipient != 'public' + next if !session[:user] + next if !session[:user]['id'] + end + + if sender_user_id + next if session[:user] && session[:user]['id'] && session[:user]['id'].to_i == sender_user_id.to_i + end Sessions.send(client_id, data) } true diff --git a/lib/sessions/event/base.rb b/lib/sessions/event/base.rb index dd98274b8..eba0821ec 100644 --- a/lib/sessions/event/base.rb +++ b/lib/sessions/event/base.rb @@ -49,6 +49,51 @@ class Sessions::Event::Base true end + def role_permission_check(role, event) + if !@session + error = { + event: "#{event}_error", + data: { + state: 'no_session', + }, + } + Sessions.send(@client_id, error) + return + end + if !@session['id'] + error = { + event: "#{event}_error", + data: { + state: 'no_session_user_id', + }, + } + Sessions.send(@client_id, error) + return + end + user = User.lookup(id: @session['id']) + if !user + error = { + event: "#{event}_error", + data: { + state: 'no_such_user', + }, + } + Sessions.send(@client_id, error) + return + end + if !user.role?(role) + error = { + event: "#{event}_error", + data: { + state: 'no_permission', + }, + } + Sessions.send(@client_id, error) + return + end + true + end + def log(level, data, client_id = nil) if !@options[:v] return if level == 'debug' diff --git a/lib/sessions/event/chat_agent_state.rb b/lib/sessions/event/chat_agent_state.rb index a73a286ab..a2661c4c4 100644 --- a/lib/sessions/event/chat_agent_state.rb +++ b/lib/sessions/event/chat_agent_state.rb @@ -4,7 +4,7 @@ class Sessions::Event::ChatAgentState < Sessions::Event::ChatBase return super if super # check if user has permissions - return if !agent_permission_check + return if !role_permission_check('Agent', 'chat') Chat::Agent.state(@session['id'], @payload['data']['active']) diff --git a/lib/sessions/event/chat_base.rb b/lib/sessions/event/chat_base.rb index 2463c952b..680f55236 100644 --- a/lib/sessions/event/chat_base.rb +++ b/lib/sessions/event/chat_base.rb @@ -23,51 +23,6 @@ class Sessions::Event::ChatBase < Sessions::Event::Base } end - def agent_permission_check - if !@session - error = { - event: 'chat_error', - data: { - state: 'no_session', - }, - } - Sessions.send(@client_id, error) - return - end - if !@session['id'] - error = { - event: 'chat_error', - data: { - state: 'no_session_user_id', - }, - } - Sessions.send(@client_id, error) - return - end - user = User.lookup(id: @session['id']) - if !user - error = { - event: 'chat_error', - data: { - state: 'no_such_user', - }, - } - Sessions.send(@client_id, error) - return - end - if !user.role?('Agent') - error = { - event: 'chat_error', - data: { - state: 'no_permission', - }, - } - Sessions.send(@client_id, error) - return - end - true - end - def current_chat_session Chat::Session.find_by(session_id: @payload['data']['session_id']) end diff --git a/lib/sessions/event/chat_session_start.rb b/lib/sessions/event/chat_session_start.rb index cc81edaaf..0b7188507 100644 --- a/lib/sessions/event/chat_session_start.rb +++ b/lib/sessions/event/chat_session_start.rb @@ -2,7 +2,7 @@ class Sessions::Event::ChatSessionStart < Sessions::Event::ChatBase def run return super if super - agent_permission_check + return if !role_permission_check('Agent', 'chat') # find first in waiting list chat_session = Chat::Session.where(state: 'waiting').order('created_at ASC').first diff --git a/lib/sessions/event/chat_status_agent.rb b/lib/sessions/event/chat_status_agent.rb index dc27868f8..b9f5e71aa 100644 --- a/lib/sessions/event/chat_status_agent.rb +++ b/lib/sessions/event/chat_status_agent.rb @@ -4,7 +4,7 @@ class Sessions::Event::ChatStatusAgent < Sessions::Event::ChatBase return super if super # check if user has permissions - return if !agent_permission_check + return if !role_permission_check('Agent', 'chat') # renew timestamps state = Chat::Agent.state(@session['id']) diff --git a/lib/sessions/event/maintenance.rb b/lib/sessions/event/maintenance.rb new file mode 100644 index 000000000..fe8ba33d9 --- /dev/null +++ b/lib/sessions/event/maintenance.rb @@ -0,0 +1,22 @@ +class Sessions::Event::Maintenance < Sessions::Event::Base + + def initialize(params) + super(params) + return if !@is_web_socket + ActiveRecord::Base.establish_connection + end + + def destroy + return if !@is_web_socket + ActiveRecord::Base.remove_connection + end + + def run + + # check if sender is admin + return if !role_permission_check('Admin', 'maintenance') + Sessions.broadcast(@payload, 'public', @session['id']) + false + end + +end diff --git a/script/build/test_slice_tests.sh b/script/build/test_slice_tests.sh index e0ffce52a..048f155a6 100755 --- a/script/build/test_slice_tests.sh +++ b/script/build/test_slice_tests.sh @@ -33,8 +33,7 @@ if [ "$LEVEL" == '1' ]; then rm test/browser/first_steps_test.rb # test/browser/form_test.rb rm test/browser/keyboard_shortcuts_test.rb - # test/browser/maintenance_app_version_test.rb - # test/browser/maintenance_message_test.rb + # test/browser/maintenance_test.rb rm test/browser/prefereces_test.rb rm test/browser/setting_test.rb # test/browser/signup_password_change_and_reset_test.rb @@ -76,7 +75,7 @@ elif [ "$LEVEL" == '2' ]; then rm test/browser/first_steps_test.rb rm test/browser/form_test.rb rm test/browser/keyboard_shortcuts_test.rb - rm test/browser/maintenance_*.rb + rm test/browser/maintenance_test.rb rm test/browser/manage_test.rb rm test/browser/prefereces_test.rb rm test/browser/setting_test.rb @@ -119,7 +118,7 @@ elif [ "$LEVEL" == '3' ]; then rm test/browser/first_steps_test.rb rm test/browser/form_test.rb rm test/browser/keyboard_shortcuts_test.rb - rm test/browser/maintenance_*.rb + rm test/browser/maintenance_test.rb rm test/browser/manage_test.rb rm test/browser/prefereces_test.rb rm test/browser/setting_test.rb @@ -162,7 +161,7 @@ elif [ "$LEVEL" == '4' ]; then rm test/browser/first_steps_test.rb rm test/browser/form_test.rb rm test/browser/keyboard_shortcuts_test.rb - rm test/browser/maintenance_*.rb + rm test/browser/maintenance_test.rb rm test/browser/manage_test.rb rm test/browser/prefereces_test.rb rm test/browser/setting_test.rb diff --git a/test/browser/maintenance_app_version_test.rb b/test/browser/maintenance_app_version_test.rb deleted file mode 100644 index df73df9e3..000000000 --- a/test/browser/maintenance_app_version_test.rb +++ /dev/null @@ -1,36 +0,0 @@ -# encoding: utf-8 -require 'browser_test_helper' - -class MaintenanceAppVersionTest < TestCase - def test_app_version - @browser = browser_instance - login( - username: 'master@example.com', - password: 'test', - url: browser_url, - ) - - sleep 10 - - execute( - js: 'App.Event.trigger("app_version", {app_version:"1234:false"})', - ) - sleep 10 - - match_not( - css: 'body', - value: 'new version', - ) - - execute( - js: 'App.Event.trigger("app_version", {app_version:"1235:true"})', - ) - sleep 5 - - match( - css: 'body', - value: 'new version', - ) - - end -end diff --git a/test/browser/maintenance_message_test.rb b/test/browser/maintenance_message_test.rb deleted file mode 100644 index 2cafd56b1..000000000 --- a/test/browser/maintenance_message_test.rb +++ /dev/null @@ -1,174 +0,0 @@ -# encoding: utf-8 -require 'browser_test_helper' - -class MaintenanceMessageTest < TestCase - def test_websocket - string = rand(99_999_999_999_999_999).to_s - title_html = "test #{string}" - title_text = "test #{string}<\/b>" - message_html = "message 1äöüß #{string}\n\n\nhttp://zammad.org" - message_text = "message 1äöüß<\/b> #{string}\n\nhttp:\/\/zammad.org" - - # check #1 - browser1 = browser_instance - login( - browser: browser1, - username: 'master@example.com', - password: 'test', - url: browser_url, - ) - - browser2 = browser_instance - login( - browser: browser2, - username: 'agent1@example.com', - password: 'test', - url: browser_url, - ) - click( - browser: browser1, - css: 'a[href="#manage"]', - ) - click( - browser: browser1, - css: 'a[href="#system/maintenance"]', - ) - - set( - browser: browser1, - css: '#content input[name="head"]', - value: title_html, - ) - set( - browser: browser1, - css: '#content textarea[name="message"]', - value: message_html, - ) - - click( - browser: browser1, - css: '#content button[type="submit"]', - ) - - watch_for( - browser: browser2, - css: '.modal', - value: title_text, - ) - watch_for( - browser: browser2, - css: '.modal', - value: message_text, - ) - - match_not( - browser: browser1, - css: 'body', - value: message_text, - ) - - click( - browser: browser2, - css: 'div.modal-header .js-close', - ) - - # check #2 - click( - browser: browser1, - css: 'a[href="#manage"]', - ) - click( - browser: browser1, - css: 'a[href="#system/maintenance"]', - ) - - set( - browser: browser1, - css: '#content input[name="head"]', - value: title_html + ' #2', - ) - set( - browser: browser1, - css: '#content textarea[name="message"]', - value: message_html + ' #2', - ) - - click( - browser: browser1, - css: '#content button[type="submit"]', - ) - - watch_for( - browser: browser2, - css: '.modal', - value: title_text + ' #2', - ) - watch_for( - browser: browser2, - css: '.modal', - value: message_text + ' #2', - ) - - match_not( - browser: browser1, - css: 'body', - value: message_text, - ) - - click( - browser: browser2, - css: 'div.modal-header .js-close', - ) - - # check #3 - click( - browser: browser1, - css: 'a[href="#manage"]', - ) - click( - browser: browser1, - css: 'a[href="#system/maintenance"]', - ) - - set( - browser: browser1, - css: '#content input[name="head"]', - value: title_html + ' #3', - ) - set( - browser: browser1, - css: '#content textarea[name="message"]', - value: message_html + ' #3', - ) - click( - browser: browser1, - css: '#content input[name="reload"] + .icon-checkbox.icon-unchecked', - ) - click( - browser: browser1, - css: '#content button[type="submit"]', - ) - - watch_for( - browser: browser2, - css: '.modal', - value: title_text + ' #3', - ) - watch_for( - browser: browser2, - css: '.modal', - value: message_text + ' #3', - ) - watch_for( - browser: browser2, - css: '.modal', - value: 'Continue session', - ) - - match_not( - browser: browser1, - css: 'body', - value: message_text, - ) - end -end diff --git a/test/browser/maintenance_test.rb b/test/browser/maintenance_test.rb new file mode 100644 index 000000000..0eca2b31e --- /dev/null +++ b/test/browser/maintenance_test.rb @@ -0,0 +1,413 @@ +# encoding: utf-8 +require 'browser_test_helper' + +class MaintenanceTest < TestCase + def test_message + string = rand(99_999_999_999_999_999).to_s + title_html = "test #{string}" + title_text = "test #{string}<\/b>" + message_html = "message 1äöüß #{string}\n\n\nhttp://zammad.org" + message_text = "message 1äöüß #{string}\n\n\nhttp://zammad.org" + + # check #1 + browser1 = browser_instance + login( + browser: browser1, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + browser2 = browser_instance + login( + browser: browser2, + username: 'agent1@example.com', + password: 'test', + url: browser_url, + ) + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + set( + browser: browser1, + css: '#content .js-Message input[name="head"]', + value: title_html, + ) + set( + browser: browser1, + css: '#content .js-Message .js-textarea[data-name="message"]', + value: message_html, + ) + + click( + browser: browser1, + css: '#content .js-Message button.js-submit', + ) + + watch_for( + browser: browser2, + css: '.modal', + value: title_text, + ) + watch_for( + browser: browser2, + css: '.modal', + value: message_text, + ) + + match_not( + browser: browser1, + css: 'body', + value: message_text, + ) + + click( + browser: browser2, + css: 'div.modal-header .js-close', + ) + + # check #2 + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + set( + browser: browser1, + css: '#content .js-Message input[name="head"]', + value: title_html + ' #2', + ) + set( + browser: browser1, + css: '#content .js-Message .js-textarea[data-name="message"]', + value: message_html + ' #2', + ) + + click( + browser: browser1, + css: '#content .js-Message button.js-submit', + ) + + watch_for( + browser: browser2, + css: '.modal', + value: title_text + ' #2', + ) + watch_for( + browser: browser2, + css: '.modal', + value: message_text + ' #2', + ) + + match_not( + browser: browser1, + css: 'body', + value: message_text, + ) + + click( + browser: browser2, + css: 'div.modal-header .js-close', + ) + + # check #3 + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + set( + browser: browser1, + css: '#content .js-Message input[name="head"]', + value: title_html + ' #3', + ) + set( + browser: browser1, + css: '#content .js-Message .js-textarea[data-name="message"]', + value: message_html + ' #3', + ) + click( + browser: browser1, + css: '#content .js-Message input[name="reload"] + .icon-checkbox.icon-unchecked', + ) + click( + browser: browser1, + css: '#content .js-Message button.js-submit', + ) + + watch_for( + browser: browser2, + css: '.modal', + value: title_text + ' #3', + ) + watch_for( + browser: browser2, + css: '.modal', + value: message_text + ' #3', + ) + watch_for( + browser: browser2, + css: '.modal', + value: 'Continue session', + ) + + match_not( + browser: browser1, + css: 'body', + value: message_text, + ) + end + + def test_login_message + browser1 = browser_instance + login( + browser: browser1, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + browser2 = browser_instance + location( + browser: browser2, + url: browser_url, + ) + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + exists_not( + browser: browser2, + css: '.js-maintenanceLogin', + ) + + string = rand(99_999_999_999_999_999).to_s + message = "test #{string}" + set( + browser: browser1, + css: '#content .js-loginPreview [data-name="message"]', + value: message, + ) + click( + browser: browser1, + css: '#global-search', + ) + + sleep 3 + switch( + browser: browser1, + css: '#content .js-loginSetting', + type: 'on', + ) + + watch_for( + browser: browser2, + css: '.js-maintenanceLogin', + value: message + ) + + switch( + browser: browser1, + css: '#content .js-loginSetting', + type: 'off', + ) + + watch_for_disappear( + browser: browser2, + css: '.js-maintenanceLogin', + ) + + end + + def test_mode + browser1 = browser_instance + login( + browser: browser1, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + browser2 = browser_instance + location( + browser: browser2, + url: browser_url, + ) + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + exists_not( + browser: browser2, + css: '.js-maintenanceMode', + ) + + switch( + browser: browser1, + css: '#content .js-modeSetting', + type: 'on', + ) + + # check warning + alert = browser1.switch_to.alert + #alert.dismiss() + alert.accept() + + watch_for( + browser: browser2, + css: '.js-maintenanceMode', + ) + + # try to logon with normal agent, should not work + login( + browser: browser2, + username: 'agent1@example.com', + password: 'test', + url: browser_url, + success: false, + ) + login( + browser: browser2, + username: 'nicole.braun@zammad.org', + password: 'test', + url: browser_url, + success: false, + ) + + # logout with admin and logon again + logout( + browser: browser1, + ) + sleep 4 + login( + browser: browser1, + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + click( + browser: browser1, + css: 'a[href="#manage"]', + ) + click( + browser: browser1, + css: 'a[href="#system/maintenance"]', + ) + + switch( + browser: browser1, + css: '#content .js-modeSetting', + type: 'off', + ) + + watch_for_disappear( + browser: browser2, + css: '.js-maintenanceMode', + ) + + # try to logon with normal agent, should work again + login( + browser: browser2, + username: 'agent1@example.com', + password: 'test', + url: browser_url, + ) + logout( + browser: browser2, + ) + sleep 4 + login( + browser: browser2, + username: 'nicole.braun@zammad.org', + password: 'test', + url: browser_url, + ) + + switch( + browser: browser1, + css: '#content .js-modeSetting', + type: 'on', + ) + # check warning + alert = browser1.switch_to.alert + #alert.dismiss() + alert.accept() + + watch_for( + browser: browser2, + css: '#login', + ) + watch_for( + browser: browser2, + css: '.js-maintenanceMode', + ) + + switch( + browser: browser1, + css: '#content .js-modeSetting', + type: 'off', + ) + + watch_for_disappear( + browser: browser2, + css: '.js-maintenanceMode', + ) + end + + def test_app_version + @browser = browser_instance + login( + username: 'master@example.com', + password: 'test', + url: browser_url, + ) + + sleep 8 + + execute( + js: 'App.Event.trigger("maintenance", {type:"app_version", app_version:"1234:false"} )', + ) + sleep 8 + + match_not( + css: 'body', + value: 'new version', + ) + + execute( + js: 'App.Event.trigger("maintenance", {type:"app_version", app_version:"1235:true"}) ', + ) + sleep 5 + + match( + css: 'body', + value: 'new version', + ) + + end + +end diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 76d778f87..be6e0c86c 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -133,6 +133,7 @@ class TestCase < Test::Unit::TestCase url: 'some url', # optional remember_me: true, # optional auto_wizard: false, # optional, in case of auto wizard, skip login + success: false, #optional ) =end @@ -194,12 +195,28 @@ class TestCase < Test::Unit::TestCase instance.find_elements(css: '#login button')[0].click sleep 4 - login = instance.find_elements(css: '.user-menu .user a')[0].attribute('title') - if login != params[:username] + login_failed = false + if !instance.find_elements(css: '.user-menu .user a')[0] + login_failed = true + else + login = instance.find_elements(css: '.user-menu .user a')[0].attribute('title') + if login != params[:username] + login_failed = true + end + end + if login_failed + if params[:success] == false + assert(true, 'login not successfull, like wanted') + return true + end screenshot(browser: instance, comment: 'login_failed') raise 'login failed' end + if params[:success] == false + raise 'login successfull but should not' + end + clues_close( browser: instance, optional: true, @@ -2693,6 +2710,78 @@ wait untill text in selector disabppears raise 'group creation failed' end +=begin + + object_manager_attribute_create( + browser: browser2, + data: { + name: 'field_name' + random, + display: 'Display Name of Field', + }, + error: 'already exists' + ) + +=end + + def object_manager_attribute_create(params = {}) + switch_window_focus(params) + log('object_manager_attribute_create', params) + + instance = params[:browser] || @browser + data = params[:data] + + click( + browser: instance, + css: 'a[href="#manage"]', + mute_log: true, + ) + click( + browser: instance, + css: 'a[href="#system/object_manager"]', + mute_log: true, + ) + sleep 4 + click( + browser: instance, + css: '#content .js-new', + mute_log: true, + ) + modal_ready + element = instance.find_elements(css: '.modal input[name=name]')[0] + element.clear + element.send_keys(data[:name]) + element = instance.find_elements(css: '.modal input[name=display]')[0] + element.clear + element.send_keys(data[:display]) + instance.find_elements(css: '.modal button.js-submit')[0].click + if params[:error] + sleep 4 + watch_for( + css: '.modal', + value: params[:error], + ) + click( + browser: instance, + css: '.modal .js-close', + mute_log: true, + ) + return + end + + (1..12).each { + element = instance.find_elements(css: 'body')[0] + text = element.text + if text =~ /#{Regexp.quote(data[:name])}/ + assert(true, 'object manager attribute created') + sleep 1 + return true + end + sleep 1 + } + screenshot(browser: instance, comment: 'object_manager_attribute_create_failed') + raise 'object manager attribute creation failed' + end + def quote(string) string_quoted = string string_quoted.gsub!(/&/, '&')