Reworked admin maintenance area (needed for restarting screen).

This commit is contained in:
Martin Edenhofer 2016-05-25 09:19:45 +02:00
parent 5bacada8ca
commit cb5fd0c2c0
31 changed files with 1025 additions and 407 deletions

View file

@ -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'

View file

@ -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

View file

@ -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' )
App.Config.set('Maintenance', { prio: 3600, name: 'Maintenance', parent: '#system', target: '#system/maintenance', controller: Index, role: ['Admin'] }, 'NavBarAdmin')

View file

@ -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')

View file

@ -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')

View file

@ -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')

View file

@ -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')

View file

@ -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

View file

@ -0,0 +1,43 @@
<div class="login branding centered darkBackground vertical">
<% if @C('maintenance_mode'): %>
<div class="hero-unit alert alert--danger"><%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %></div>
<% end %>
<% if !@logoChange || @C('maintenance_login'): %>
<form>
<div contenteditable id="maintenance-message" data-name="message" class="hero-unit alert alert--success js-textarea js-Login" <% if !@C('maintenance_login'): %>style="opacity: 0.5;"<% end %>><%- @C('maintenance_login_message') %></div>
</form>
<% end %>
<div class="hero-unit">
<% if @logoChange: %>
<img class="logo-preview" src="<%= @logoUrl %>">
<div class="logo-preview-placeholder"><%- @T('Your Logo') %></div>
<div class="centered">
<div class="btn btn--success fileUpload"><%- @T('Change') %><input type="file" class="js-upload" name="logo" accept="image/*"></div>
</div>
<% else: %>
<img class="company-logo" src="<%= @logoUrl %>">
<% end %>
<div class="form-group">
<label for="username"><%- @Ti('Username / email') %></label>
<input id="username" name="username" type="text" class="form-control" value="<%= @S('login') %>" autocapitalize="off" disabled="disabled"/>
</div>
<div class="form-group">
<label for="password"><%- @Ti('Password') %></label>
<input id="password" name="password" type="password" class="form-control" value="some_pass" disabled="disabled"/>
</div>
<div class="form-group">
<label><input name="remember_me" value="1" type="checkbox" disabled="disabled"/> <%- @T('Remember me') %></label>
</div>
<div class="form-controls">
<button class="btn btn--primary" type="submit" disabled="disabled"><%- @T('Sign in') %></button>
</div>
</div>
</div>

View file

@ -1,56 +1,63 @@
<div class="login fullscreen">
<div class="fullscreen-center">
<div class="fullscreen-body">
<p><%- @T( 'Login with %s', @C( 'fqdn' ) ) %></p>
<p><%- @T('Login with %s', @C('fqdn')) %></p>
<% if @C('maintenance_mode'): %>
<div class="hero-unit alert alert--danger js-maintenanceMode"><%- @T('Zammad is currently in maintenance mode. Only administrators can login. Please wait until the maintenance window is over.') %></div>
<% end %>
<% if @C('maintenance_login') && @C('maintenance_login_message'): %>
<div class="hero-unit alert alert--success js-maintenanceLogin"><%- @C('maintenance_login_message') %></div>
<% end %>
<div class="hero-unit">
<img class="company-logo" src="<%= @logoUrl %>" alt="<%= @C( 'product_name' ) %>">
<img class="company-logo" src="<%= @logoUrl %>" alt="<%= @C('product_name') %>">
<form id="login">
<div class="form-group">
<div class="formGroup-label">
<label for="username"><%- @Ti( 'Username / email' ) %></label>
<label for="username"><%- @Ti('Username / email') %></label>
</div>
<input id="username" name="username" type="text" class="form-control" value="<%= @item.username %>" autocapitalize="off" />
</div>
<div class="form-group">
<div class="formGroup-label">
<label for="password"><%- @Ti( 'Password' ) %></label>
<label for="password"><%- @Ti('Password') %></label>
</div>
<input id="password" name="password" type="password" class="form-control"/>
</div>
<div class="form-group">
<!--
<label for="remember_me"><%- @Ti( 'Remember me' ) %></label>
<label for="remember_me"><%- @Ti('Remember me') %></label>
<input id="remember_me" name="remember_me" value="1" type="checkbox"/>
-->
<label class="inline-label checkbox-replacement">
<input name="remember_me" value="1" type="checkbox">
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%- @T( 'Remember me' ) %></span>
<span class="label-text"><%- @T('Remember me') %></span>
</label>
</div>
<div class="form-controls">
<button class="btn btn--primary" type="submit"><%- @T( 'Sign in' ) %></button>
<button class="btn btn--primary" type="submit"><%- @T('Sign in') %></button>
<% if @C('user_lost_password'): %>
<a href="#password_reset" class="btn btn--text btn--secondary align-right"><%- @T( 'Forgot password?' ) %></a>
<a href="#password_reset" class="btn btn--text btn--secondary align-right"><%- @T('Forgot password?') %></a>
<% end %>
</div>
<% if !_.isEmpty( @auth_providers ): %>
<% if !_.isEmpty(@auth_providers): %>
<div class="separator">
<span class="separator-text"><%- @T( 'or sign in using' ) %></span>
<span class="separator-text"><%- @T('or sign in using') %></span>
</div>
<div class="auth-providers">
<% for auth_provider in @auth_providers: %>
<a class="auth-provider auth-provider--<%= auth_provider.class %>" href="<%= auth_provider.url %>">
<%- @Icon("#{auth_provider.class}-button", 'provider-icon') %>
<span class="provider-name"><%- @T( auth_provider.name ) %></span>
<span class="provider-name"><%- @T(auth_provider.name) %></span>
</a>
<% end %>
</div>
@ -59,23 +66,23 @@
</div>
<p>
<%- @T( "You're already registered with your email adress if you've been in touch with our support team.") %><br>
<%- @T("You're already registered with your email adress if you've been in touch with our support team.") %><br>
<% if @C('user_lost_password'): %>
<%- @T( "You can request your password") %> <a href="#password_reset"><%- @T( "here") %></a>.
<%- @T('You can request your password') %> <a href="#password_reset"><%- @T('here') %></a>.
<% end %>
</p>
<% if @C('user_create_account'): %>
<hr>
<p>
<a href="#signup"><%- @T( 'Register as a new customer' ) %></a>
<a href="#signup"><%- @T('Register as a new customer') %></a>
</p>
<% end %>
</div>
</div>
<div class="poweredBy">
<%- @Icon('logo') %>
<%- @T("Powered by") %>
<%- @T('Powered by') %>
<%- @Icon('logotype', 'logotype') %>
</div>
</div>

View file

@ -1,30 +1,58 @@
<div class="page-header">
<div class="page-header-title">
<h1><%- @T('Maintenance Message') %><small></small></h1>
<h1><%- @T('Maintenance') %><small></small></h1>
</div>
</div>
<div class="page-content">
<form id="maintenanceForm">
<div class="form-group">
<label for="maintenance-title"><%- @T('Title') %></label>
<div class="controls">
<input type="text" id="maintenance-title" name="head" class="form-control" required>
<div class="settings-entry">
<div class="page-header-title">
<div class="zammad-switch zammad-switch--small js-modeSetting">
<input name="chat" type="checkbox" id="setting-mode" <% if @C('maintenance_mode'): %>checked<% end %>>
<label for="setting-mode"></label>
</div>
<h2><%- @T('Mode') %></h2>
</div>
<div class="form-group">
<label for="maintenance-message"><%- @T('Message') %></label>
<div class="controls">
<textarea id="maintenance-message" name="message" class="form-control" rows="8" required></textarea>
<p>⚠ <%- @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_.') %></p>
</div>
<div class="settings-entry">
<div class="page-header-title">
<div class="zammad-switch zammad-switch--small js-loginSetting">
<input name="chat" type="checkbox" id="setting-login" <% if @C('maintenance_login'): %>checked<% end %>>
<label for="setting-login"></label>
</div>
<h2>@<%- @T('Login') %></h2>
</div>
<div class="form-group">
<label class="inline-label checkbox-replacement">
<input name="reload" type="checkbox" value="1">
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%- @T('Reload application') %></span>
</label>
</div>
<button type="submit" class="btn btn--primary submit"><%- @T('Send to clients') %></button>
</form>
<p><%- @T('Put a message on the login page. To change it, click on the text area below and change it inline.') %></p>
<div class="js-loginPreview"></div>
</div>
<div class="settings-entry">
<h2><%- @T('Message') %></h2>
<p><%- @T('Send a message to all logged in users.') %></p>
<form class="js-Message">
<div class="form-group">
<label for="maintenance-title"><%- @T('Title') %></label>
<div class="controls">
<input type="text" id="maintenance-title" name="head" class="form-control" required>
</div>
</div>
<div class="form-group">
<label for="maintenance-message"><%- @T('Message') %></label>
<div class="controls">
<div contenteditable id="maintenance-message" data-name="message" class="form-control form-control--multiline js-textarea richtext-content"></div>
</div>
</div>
<div class="form-group">
<label class="inline-label checkbox-replacement">
<input name="reload" type="checkbox" value="1">
<%- @Icon('checkbox', 'icon-unchecked') %>
<%- @Icon('checkbox-checked', 'icon-checked') %>
<span class="label-text"><%- @T('Reload application') %></span>
</label>
</div>
<button class="btn btn--primary js-submit"><%- @T('Send to clients') %></button>
</form>
</div>
</div>

View file

@ -1,36 +1,8 @@
<form class="settings-entry" id="<%= @setting.name %>">
<h2><%- @T( @setting.title ) %></h2>
<p><%- @T( @setting.description ) %></p>
<div class="login branding centered darkBackground">
<div class="hero-unit">
<img class="logo-preview" src="<%= @logoUrl %>">
<div class="logo-preview-placeholder"><%- @T('Your Logo') %></div>
<div class="centered">
<div class="btn btn--success fileUpload"><%- @T('Change') %><input type="file" class="js-upload" name="logo" accept="image/*"></div>
</div>
<div class="form-group">
<label for="username"><%- @Ti( 'Username / email' ) %></label>
<input id="username" name="username" type="text" class="form-control" value="<%= @S('login') %>" autocapitalize="off" disabled="disabled"/>
</div>
<div class="form-group">
<label for="password"><%- @Ti( 'Password' ) %></label>
<input id="password" name="password" type="password" class="form-control" value="some_pass" disabled="disabled"/>
</div>
<div class="form-group">
<label><input name="remember_me" value="1" type="checkbox" disabled="disabled"/> <%- @T( 'Remember me' ) %></label>
</div>
<div class="form-controls">
<button class="btn btn--primary" type="submit" disabled="disabled"><%- @T( 'Sign in' ) %></button>
</div>
</div>
</div>
<h2><%- @T(@setting.title) %></h2>
<p><%- @T(@setting.description) %></p>
<div class="js-loginPreview"></div>
<div class="setting-controls">
<button type="submit" class="btn btn--primary"><%- @T( 'Submit' ) %></button>
<button type="submit" class="btn btn--primary"><%- @T('Submit') %></button>
</div>
</form>

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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',

View file

@ -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,
}
}

View file

@ -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

View file

@ -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'

View file

@ -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'])

View file

@ -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

View file

@ -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

View file

@ -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'])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <b>#{string}</b>"
title_text = "test <b>#{string}<\/b>"
message_html = "message <b>1äöüß</b> #{string}\n\n\nhttp://zammad.org"
message_text = "message <b>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

View file

@ -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 <b>#{string}</b>"
title_text = "test <b>#{string}<\/b>"
message_html = "message <b>1äöüß</b> #{string}\n\n\nhttp://zammad.org"
message_text = "message <b>1äöüß</b> #{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 <b>#{string}</b>"
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

View file

@ -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!(/&/, '&amp;')