mirror of
https://github.com/opf/openproject.git
synced 2026-06-13 19:20:00 +00:00
Merge pull request #18734 from opf/chore/primerize-auth-settings
Primerize authentication settings page
This commit is contained in:
@@ -33,18 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
header.with_breadcrumbs(breadcrumb_items)
|
||||
|
||||
if @tabs.present?
|
||||
header.with_tab_nav(label: nil) do |tab_nav|
|
||||
@tabs.each do |tab|
|
||||
tab_nav.with_tab(selected: selected_tab(@tabs) == tab, href: tab[:path]) do |t|
|
||||
feature = tab[:enterprise_feature]
|
||||
|
||||
if feature && !EnterpriseToken.allows_to?(feature)
|
||||
t.with_icon(icon: :"op-enterprise-addons", classes: "upsell-colored")
|
||||
end
|
||||
t.with_text { I18n.t(tab[:label]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
helpers.render_tab_header_nav(header, @tabs)
|
||||
end
|
||||
end
|
||||
%>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
class ApplicationForm < Primer::Forms::Base
|
||||
def self.settings_form
|
||||
form do |f|
|
||||
f = SettingsFormDecorator.new(f)
|
||||
f = Settings::FormDecorator.new(f)
|
||||
yield f
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
# Decorates a form object to provide a more convenient interface for
|
||||
# rendering settings.
|
||||
#
|
||||
# It automatically sets the label, value, and disabled properties from the
|
||||
# setting name and its definition attributes.
|
||||
module Settings
|
||||
class FormDecorator
|
||||
include ::SettingsHelper
|
||||
include ::ApplicationHelper
|
||||
include FormHelper
|
||||
|
||||
attr_reader :form
|
||||
|
||||
# Initializes a new Settings::FormDecorator
|
||||
#
|
||||
# @param form [Object] The form object to be decorated
|
||||
def initialize(form)
|
||||
@form = form
|
||||
end
|
||||
|
||||
def method_missing(method, ...)
|
||||
form.send(method, ...)
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, include_private = false)
|
||||
form.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
# Creates a text field input for a setting.
|
||||
#
|
||||
# The text field label is set from translating the key "setting_<name>".
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param options [Hash] Additional options for the text field
|
||||
# @return [Object] The text field input
|
||||
def text_field(name:, **options)
|
||||
options.reverse_merge!(
|
||||
label: setting_label(name),
|
||||
value: setting_value(name),
|
||||
disabled: setting_disabled?(name)
|
||||
)
|
||||
form.text_field(name:, **options)
|
||||
end
|
||||
|
||||
# Creates a check box input for a setting.
|
||||
#
|
||||
# The check box label is set from translating the key "setting_<name>".
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param options [Hash] Additional options for the check box
|
||||
# @return [Object] The check box input
|
||||
def check_box(name:, **options)
|
||||
options.reverse_merge!(
|
||||
label: setting_label(name),
|
||||
checked: setting_value(name),
|
||||
disabled: setting_disabled?(name)
|
||||
)
|
||||
form.check_box(name:, **options)
|
||||
end
|
||||
|
||||
# Creates a radio button group for a setting.
|
||||
#
|
||||
# The radio button group label is set from translating the key
|
||||
# "setting_<name>". The radio button label are set from translating the
|
||||
# key "setting_<name>_<value>". The caption is set from translating the
|
||||
# key "setting_<name>_<value>_caption_html", which will be rendered as HTML,
|
||||
# or "setting_<name>_<value>_caption", or nothing if none of the above
|
||||
# are defined.
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param values [Hash|Array] The values for the radio buttons. Default to the
|
||||
# setting's allowed values.
|
||||
# If a hash is provided, it is assumed it provides a :name (to derive the labels) and a :value key.
|
||||
# Other keys are used as arguments to the radio_button.
|
||||
# @param disabled [Boolean] Force the radio button group to be disabled when
|
||||
# true, will be disabled if the setting is not writable when false (default)
|
||||
# @param button_options [Hash] Options for individual radio buttons
|
||||
# @param options [Hash] Additional options for the radio button group
|
||||
# @return [Object] The radio button group
|
||||
def radio_button_group(name:, values: [], disabled: false, button_options: {}, **options) # rubocop:disable Metrics/AbcSize
|
||||
values = values.presence || setting_allowed_values(name)
|
||||
radio_group_options = options.reverse_merge(
|
||||
label: setting_label(name),
|
||||
disabled: disabled || setting_disabled?(name)
|
||||
)
|
||||
form.radio_button_group(
|
||||
name:,
|
||||
**radio_group_options
|
||||
) do |radio_group|
|
||||
values.each do |value|
|
||||
args =
|
||||
if value.is_a?(Hash)
|
||||
value.reverse_merge(
|
||||
checked: setting_value(name) == value[:value],
|
||||
autocomplete: "off",
|
||||
label: setting_label(name, value[:name]),
|
||||
caption: setting_caption(name, value[:name])
|
||||
)
|
||||
else
|
||||
{
|
||||
value:,
|
||||
checked: setting_value(name) == value,
|
||||
autocomplete: "off",
|
||||
label: setting_label(name, value),
|
||||
caption: setting_caption(name, value)
|
||||
}
|
||||
end
|
||||
|
||||
radio_group.radio_button(**button_options.reverse_merge(args))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def multi_language_text_select(name:, current_language: I18n.locale.to_s)
|
||||
# Add select list to switch
|
||||
form.select_list(
|
||||
name: :"#{name}_lang", # Should be excluded by settings params
|
||||
input_width: :small,
|
||||
id: "lang-for-#{name}",
|
||||
class: "lang-select-switch",
|
||||
label: setting_label(name),
|
||||
caption: setting_caption(name),
|
||||
include_blank: false
|
||||
) do |select|
|
||||
lang_options_for_select(false).each do |label, value|
|
||||
select.option(
|
||||
value:,
|
||||
label:,
|
||||
selected: value == current_language
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
form.fields_for(name) do |builder|
|
||||
MultiLangForm.new(builder, name:, current_language:)
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a save button to submit the form
|
||||
#
|
||||
# @return [Object] The submit button
|
||||
def submit
|
||||
form.submit(name: "submit",
|
||||
label: I18n.t("button_save"),
|
||||
scheme: :primary)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,90 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
module Settings
|
||||
module FormHelper
|
||||
# Returns a translated string for a setting name.
|
||||
#
|
||||
# The translation key is "setting_<name>". Add additional names to the key
|
||||
# to allow for translations with more context:
|
||||
# "setting_<name>_<param2>_<param3>_...".
|
||||
#
|
||||
# @param names [Array<String | Symbol>] The name(s) of the setting
|
||||
# @return [String] The translated label
|
||||
def setting_label(*names)
|
||||
I18n.t("setting_#{names.join('_')}")
|
||||
end
|
||||
|
||||
# Generates an HTML-safe caption for a setting.
|
||||
#
|
||||
# The translation key is "setting_<name>_caption". If not present, it will
|
||||
# return nil.
|
||||
#
|
||||
# The translation will be marked as html_safe automatically if it ends with
|
||||
# "_html", allowing to have HTML in the caption.
|
||||
#
|
||||
# Add additional names to the key to allow for translations with more context:
|
||||
# "setting_<name>_<context>_caption_html" for instance.
|
||||
#
|
||||
# @param names [Array<Symbol>] The name(s) of the setting
|
||||
# @return [String] The translated HTML-safe caption
|
||||
def setting_caption(*names)
|
||||
I18n.t("setting_#{names.join('_')}_caption_html", default: nil)&.html_safe \
|
||||
|| I18n.t("setting_#{names.join('_')}_caption", default: nil)
|
||||
end
|
||||
|
||||
# Retrieves the current value of a setting
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Object] The value of the setting
|
||||
def setting_value(name)
|
||||
Setting[name]
|
||||
end
|
||||
|
||||
# Retrieves the allowed values for a setting's definition
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Array] The allowed values for the setting
|
||||
def setting_allowed_values(name)
|
||||
Settings::Definition[name].allowed
|
||||
end
|
||||
|
||||
# Checks if a setting is disabled.
|
||||
#
|
||||
# Any non-writable setting set by environment variables will be considered
|
||||
# disabled.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Boolean] `true` if the setting is disabled, `false` otherwise
|
||||
def setting_disabled?(name)
|
||||
!Setting.send(:"#{name}_writable?")
|
||||
end
|
||||
end
|
||||
end
|
||||
+36
-28
@@ -1,4 +1,6 @@
|
||||
#-- copyright
|
||||
# frozen_string_literal: true
|
||||
|
||||
# -- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
@@ -24,38 +26,44 @@
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
# ++
|
||||
|
||||
require "spec_helper"
|
||||
module Settings
|
||||
class MultiLangForm < ApplicationForm
|
||||
include FormHelper
|
||||
|
||||
RSpec.describe "admin/settings/authentication_settings/show" do
|
||||
context "with password login enabled" do
|
||||
before do
|
||||
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(false)
|
||||
render
|
||||
attr_reader :name, :current_language
|
||||
|
||||
def initialize(name:, current_language:)
|
||||
super()
|
||||
|
||||
@name = name
|
||||
@current_language = current_language
|
||||
end
|
||||
|
||||
it "shows password settings" do
|
||||
expect(rendered).to have_text I18n.t("label_password_lost")
|
||||
end
|
||||
form do |f|
|
||||
# Add hidden languages
|
||||
Redmine::I18n.valid_languages.each do |lang|
|
||||
f.hidden(
|
||||
name: lang,
|
||||
value: Setting.send(name)[lang],
|
||||
id: "lang-for-#{name}-#{lang}"
|
||||
)
|
||||
end
|
||||
|
||||
it "shows automated user blocking options" do
|
||||
expect(rendered).to have_text I18n.t("settings.brute_force_prevention")
|
||||
end
|
||||
end
|
||||
|
||||
context "with password login disabled" do
|
||||
before do
|
||||
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
|
||||
render
|
||||
end
|
||||
|
||||
it "does not show password settings" do
|
||||
expect(rendered).to have_no_text I18n.t("label_password_lost")
|
||||
end
|
||||
|
||||
it "does not show automated user blocking options" do
|
||||
expect(rendered).to have_no_text I18n.t("settings.brute_force_prevention")
|
||||
# Add WYSIWYG
|
||||
f.rich_text_area(
|
||||
name: current_language,
|
||||
value: setting_value(name)[current_language],
|
||||
label: setting_label(name),
|
||||
disabled: setting_disabled?(name),
|
||||
visually_hide_label: true,
|
||||
rich_text_options: {
|
||||
text_area_id: "settings-#{name}",
|
||||
turboMode: true,
|
||||
showAttachments: false
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,200 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
# Decorates a form object to provide a more convenient interface for
|
||||
# rendering settings.
|
||||
#
|
||||
# It automatically sets the label, value, and disabled properties from the
|
||||
# setting name and its definition attributes.
|
||||
class SettingsFormDecorator
|
||||
attr_reader :form
|
||||
|
||||
# Initializes a new SettingsFormDecorator
|
||||
#
|
||||
# @param form [Object] The form object to be decorated
|
||||
def initialize(form)
|
||||
@form = form
|
||||
end
|
||||
|
||||
def method_missing(method, ...)
|
||||
form.send(method, ...)
|
||||
end
|
||||
|
||||
def respond_to_missing?(method, include_private = false)
|
||||
form.respond_to?(method, include_private)
|
||||
end
|
||||
|
||||
# Creates a text field input for a setting.
|
||||
#
|
||||
# The text field label is set from translating the key "setting_<name>".
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param options [Hash] Additional options for the text field
|
||||
# @return [Object] The text field input
|
||||
def text_field(name:, **options)
|
||||
options.reverse_merge!(
|
||||
label: setting_label(name),
|
||||
value: setting_value(name),
|
||||
disabled: setting_disabled?(name)
|
||||
)
|
||||
form.text_field(name:, **options)
|
||||
end
|
||||
|
||||
# Creates a check box input for a setting.
|
||||
#
|
||||
# The check box label is set from translating the key "setting_<name>".
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param options [Hash] Additional options for the check box
|
||||
# @return [Object] The check box input
|
||||
def check_box(name:, **options)
|
||||
options.reverse_merge!(
|
||||
label: setting_label(name),
|
||||
checked: setting_value(name),
|
||||
disabled: setting_disabled?(name)
|
||||
)
|
||||
form.check_box(name:, **options)
|
||||
end
|
||||
|
||||
# Creates a radio button group for a setting.
|
||||
#
|
||||
# The radio button group label is set from translating the key
|
||||
# "setting_<name>". The radio button label are set from translating the
|
||||
# key "setting_<name>_<value>". The caption is set from translating the
|
||||
# key "setting_<name>_<value>_caption_html", which will be rendered as HTML,
|
||||
# or "setting_<name>_<value>_caption", or nothing if none of the above
|
||||
# are defined.
|
||||
#
|
||||
# Any options passed to this method will override the default options.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @param values [Array] The values for the radio buttons. Default to the
|
||||
# setting's allowed values.
|
||||
# @param disabled [Boolean] Force the radio button group to be disabled when
|
||||
# true, will be disabled if the setting is not writable when false (default)
|
||||
# @param button_options [Hash] Options for individual radio buttons
|
||||
# @param options [Hash] Additional options for the radio button group
|
||||
# @return [Object] The radio button group
|
||||
def radio_button_group(name:, values: [], disabled: false, button_options: {}, **options)
|
||||
values = values.presence || setting_allowed_values(name)
|
||||
radio_group_options = options.reverse_merge(
|
||||
label: setting_label(name),
|
||||
disabled: disabled || setting_disabled?(name)
|
||||
)
|
||||
form.radio_button_group(
|
||||
name:,
|
||||
**radio_group_options
|
||||
) do |radio_group|
|
||||
values.each do |value|
|
||||
radio_group.radio_button(
|
||||
**button_options.reverse_merge(
|
||||
value:,
|
||||
checked: setting_value(name) == value,
|
||||
autocomplete: "off",
|
||||
label: setting_label(name, value),
|
||||
caption: setting_caption(name, value)
|
||||
)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Creates a save button to submit the form
|
||||
#
|
||||
# @return [Object] The submit button
|
||||
def submit
|
||||
form.submit(name: "submit",
|
||||
label: I18n.t("button_save"),
|
||||
scheme: :primary)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# Returns a translated string for a setting name.
|
||||
#
|
||||
# The translation key is "setting_<name>". Add additional names to the key
|
||||
# to allow for translations with more context:
|
||||
# "setting_<name>_<param2>_<param3>_...".
|
||||
#
|
||||
# @param names [Array<String | Symbol>] The name(s) of the setting
|
||||
# @return [String] The translated label
|
||||
def setting_label(*names)
|
||||
I18n.t("setting_#{names.join('_')}")
|
||||
end
|
||||
|
||||
# Generates an HTML-safe caption for a setting.
|
||||
#
|
||||
# The translation key is "setting_<name>_caption". If not present, it will
|
||||
# return nil.
|
||||
#
|
||||
# The translation will be marked as html_safe automatically if it ends with
|
||||
# "_html", allowing to have HTML in the caption.
|
||||
#
|
||||
# Add additional names to the key to allow for translations with more context:
|
||||
# "setting_<name>_<context>_caption_html" for instance.
|
||||
#
|
||||
# @param names [Array<Symbol>] The name(s) of the setting
|
||||
# @return [String] The translated HTML-safe caption
|
||||
def setting_caption(*names)
|
||||
I18n.t("setting_#{names.join('_')}_caption_html", default: nil)&.html_safe \
|
||||
|| I18n.t("setting_#{names.join('_')}_caption", default: nil)
|
||||
end
|
||||
|
||||
# Retrieves the current value of a setting
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Object] The value of the setting
|
||||
def setting_value(name)
|
||||
Setting[name]
|
||||
end
|
||||
|
||||
# Retrieves the allowed values for a setting's definition
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Array] The allowed values for the setting
|
||||
def setting_allowed_values(name)
|
||||
Settings::Definition[name].allowed
|
||||
end
|
||||
|
||||
# Checks if a setting is disabled.
|
||||
#
|
||||
# Any non-writable setting set by environment variables will be considered
|
||||
# disabled.
|
||||
#
|
||||
# @param name [Symbol] The name of the setting
|
||||
# @return [Boolean] `true` if the setting is disabled, `false` otherwise
|
||||
def setting_disabled?(name)
|
||||
!Setting.send(:"#{name}_writable?")
|
||||
end
|
||||
end
|
||||
@@ -39,8 +39,23 @@ module TabsHelper
|
||||
end
|
||||
end
|
||||
|
||||
def render_tab_header_nav(header, tabs)
|
||||
header.with_tab_nav(label: nil) do |tab_nav|
|
||||
tabs.each do |tab|
|
||||
tab_nav.with_tab(selected: selected_tab(tabs) == tab, href: tab[:path]) do |t|
|
||||
feature = tab[:enterprise_feature]
|
||||
|
||||
if feature && !EnterpriseToken.allows_to?(feature)
|
||||
t.with_icon(icon: :"op-enterprise-addons", classes: "upsell-colored")
|
||||
end
|
||||
t.with_text { I18n.t(tab[:label]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def selected_tab(tabs)
|
||||
tabs.detect { |t| t[:name] == params[:tab] } || tabs.first
|
||||
tabs.detect { |t| t[:name].to_s == params[:tab].to_s } || tabs.first
|
||||
end
|
||||
|
||||
def tabs_for_key(key, params = {})
|
||||
|
||||
@@ -54,8 +54,12 @@ class Setting
|
||||
value key: :disabled
|
||||
end
|
||||
|
||||
def self.selected?(val)
|
||||
key(value: Setting.self_registration) == val.to_sym
|
||||
end
|
||||
|
||||
def self.disabled?
|
||||
key(value: Setting.self_registration) == :disabled
|
||||
selected?(:disabled)
|
||||
end
|
||||
|
||||
def self.enabled?
|
||||
@@ -67,7 +71,7 @@ class Setting
|
||||
end
|
||||
|
||||
def self.by_email?
|
||||
key(value: Setting.self_registration) == :activation_by_email
|
||||
selected?(:activation_by_email)
|
||||
end
|
||||
|
||||
def self.manual
|
||||
@@ -75,7 +79,7 @@ class Setting
|
||||
end
|
||||
|
||||
def self.manual?
|
||||
key(value: Setting.self_registration) == :manual_activation
|
||||
selected?(:manual_activation)
|
||||
end
|
||||
|
||||
def self.automatic
|
||||
@@ -83,7 +87,7 @@ class Setting
|
||||
end
|
||||
|
||||
def self.automatic?
|
||||
key(value: Setting.self_registration) == :automatic_activation
|
||||
selected?(:automatic_activation)
|
||||
end
|
||||
|
||||
def self.unsupervised_registration?
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
admin_settings_primer_form_with(scope: :settings,
|
||||
url: admin_settings_authentication_path(tab: params[:tab]),
|
||||
method: :patch) do |f|
|
||||
render_inline_settings_form(f) do |form|
|
||||
form.select_list(
|
||||
name: :omniauth_direct_login_provider,
|
||||
input_width: :medium,
|
||||
label: I18n.t(:setting_omniauth_direct_login_provider),
|
||||
caption: I18n.t(
|
||||
"settings.authentication.omniauth_direct_login_hint_html",
|
||||
internal_path: internal_signin_url
|
||||
).html_safe,
|
||||
include_blank: I18n.t(:label_none_parentheses)
|
||||
) do |select|
|
||||
AuthProvider
|
||||
.where(available: true)
|
||||
.order("lower(display_name) ASC")
|
||||
.select(:type, :display_name, :slug)
|
||||
.to_a
|
||||
.each do |provider|
|
||||
select.option(
|
||||
value: provider.slug,
|
||||
label: "#{provider.display_name} (#{provider.human_type})",
|
||||
selected: Setting.omniauth_direct_login_provider == provider.slug
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
form.select_list(
|
||||
name: :autologin,
|
||||
input_width: :medium,
|
||||
label: I18n.t(:setting_autologin),
|
||||
) do |select|
|
||||
select.option(
|
||||
value: 0,
|
||||
label: I18n.t(:label_disabled),
|
||||
selected: Setting.autologin == 0
|
||||
)
|
||||
|
||||
Settings::Definition[:autologin].allowed.each do |days|
|
||||
select.option(
|
||||
value: days,
|
||||
label: I18n.t("datetime.distance_in_words.x_days", count: days),
|
||||
selected: Setting.autologin == days
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
form.check_box(name: :session_ttl_enabled)
|
||||
|
||||
form.text_field(name: :session_ttl,
|
||||
type: :number,
|
||||
size: 6,
|
||||
min: 0,
|
||||
caption: I18n.t("setting_session_ttl_hint"),
|
||||
trailing_visual: { text: { text: I18n.t(:label_minute_plural) } },
|
||||
input_width: :small)
|
||||
|
||||
form.check_box(name: :log_requesting_user)
|
||||
|
||||
form.text_field(name: :after_first_login_redirect_url,
|
||||
caption: I18n.t(:setting_after_first_login_redirect_url_text_html).html_safe,
|
||||
input_width: :large)
|
||||
|
||||
form.text_field(name: :after_login_default_redirect_url,
|
||||
caption: I18n.t(:setting_after_login_default_redirect_url_text_html).html_safe,
|
||||
input_width: :large)
|
||||
|
||||
form.submit
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,117 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
admin_settings_primer_form_with(scope: :settings,
|
||||
url: admin_settings_authentication_path(tab: params[:tab]),
|
||||
data: { turbo_method: :patch },
|
||||
method: :patch) do |f|
|
||||
render_inline_settings_form(f) do |form|
|
||||
disabled = OpenProject::Configuration.disable_password_login?
|
||||
|
||||
if disabled
|
||||
form.html_content do
|
||||
render Primer::Alpha::Banner.new(
|
||||
scheme: :default,
|
||||
icon: :info,
|
||||
my: 2
|
||||
) do
|
||||
I18n.t(
|
||||
:note_password_login_disabled,
|
||||
configuration: static_link_to(:disable_password_login, label: I18n.t('label_configuration'))
|
||||
).html_safe
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
form.text_field name: :password_min_length,
|
||||
disabled:,
|
||||
type: :number,
|
||||
size: 6,
|
||||
min: 1,
|
||||
input_width: :small
|
||||
|
||||
form.check_box_group(name: :password_active_rules,
|
||||
disabled:,
|
||||
label: I18n.t(:setting_password_active_rules)) do |group|
|
||||
OpenProject::Passwords::Evaluator.known_rules.each do |value|
|
||||
group.check_box(value:,
|
||||
label: I18n.t("label_password_rule_#{value}"),
|
||||
checked: OpenProject::Passwords::Evaluator.active_rule?(value),
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
form.text_field(name: :password_min_adhered_rules,
|
||||
type: :number,
|
||||
disabled:,
|
||||
size: 6,
|
||||
min: 0,
|
||||
max: 4,
|
||||
input_width: :small)
|
||||
|
||||
form.text_field(name: :password_days_valid,
|
||||
type: :number,
|
||||
disabled:,
|
||||
size: 6,
|
||||
min: 0,
|
||||
caption: I18n.t(:text_hint_disable_with_0),
|
||||
input_width: :small)
|
||||
|
||||
form.text_field(name: :password_count_former_banned,
|
||||
type: :number,
|
||||
disabled:,
|
||||
size: 6,
|
||||
min: 0,
|
||||
input_width: :small)
|
||||
|
||||
form.check_box(name: :lost_password,
|
||||
disabled:)
|
||||
|
||||
form.text_field(name: :brute_force_block_after_failed_logins,
|
||||
type: :number,
|
||||
disabled:,
|
||||
size: 6,
|
||||
min: 0,
|
||||
caption: I18n.t(:text_hint_disable_with_0),
|
||||
input_width: :small)
|
||||
|
||||
form.text_field(name: :brute_force_block_minutes,
|
||||
type: :number,
|
||||
disabled:,
|
||||
size: 6,
|
||||
min: 0,
|
||||
caption: I18n.t(:text_hint_disable_with_0),
|
||||
trailing_visual: { text: { text: I18n.t(:label_minute_plural) } },
|
||||
input_width: :small)
|
||||
|
||||
form.submit
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -0,0 +1,75 @@
|
||||
<%#-- copyright
|
||||
OpenProject is an open source project management software.
|
||||
Copyright (C) the OpenProject GmbH
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License version 3.
|
||||
|
||||
OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
Copyright (C) 2010-2013 the ChiliProject Team
|
||||
|
||||
This program is free software; you can redistribute it and/or
|
||||
modify it under the terms of the GNU General Public License
|
||||
as published by the Free Software Foundation; either version 2
|
||||
of the License, or (at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
|
||||
See COPYRIGHT and LICENSE files for more details.
|
||||
|
||||
++#%>
|
||||
|
||||
<%=
|
||||
admin_settings_primer_form_with(scope: :settings,
|
||||
url: admin_settings_authentication_path(tab: params[:tab]),
|
||||
data: { turbo_method: :patch },
|
||||
method: :patch) do |f|
|
||||
render_inline_settings_form(f) do |form|
|
||||
form.check_box(
|
||||
name: :login_required,
|
||||
caption: I18n.t(:setting_login_required_caption),
|
||||
)
|
||||
|
||||
form.radio_button_group(
|
||||
name: :self_registration,
|
||||
label: I18n.t(:setting_self_registration),
|
||||
caption: I18n.t(:setting_self_registration_caption),
|
||||
values: Setting::SelfRegistration::VALUES.map { |name, value| { name:, value: } }
|
||||
)
|
||||
|
||||
if Setting::SelfRegistration.unsupervised_registration?
|
||||
form.html_content do
|
||||
render Primer::Alpha::Banner.new(
|
||||
scheme: :warning,
|
||||
icon: :alert,
|
||||
my: 2
|
||||
) do
|
||||
I18n.t(:setting_self_registration_warning)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
form.text_field(
|
||||
type: :number,
|
||||
min: 0,
|
||||
max: 100,
|
||||
input_width: :small,
|
||||
trailing_visual: { text: { text: I18n.t("datetime.units.day.other") } },
|
||||
name: :invitation_expiration_days,
|
||||
caption: I18n.t(:setting_invitation_expiration_days_caption),
|
||||
)
|
||||
|
||||
form.multi_language_text_select(name: :registration_footer)
|
||||
|
||||
form.submit
|
||||
end
|
||||
end
|
||||
%>
|
||||
@@ -28,167 +28,41 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
++#%>
|
||||
<% html_title t(:label_administration), t(:label_authentication) -%>
|
||||
|
||||
<% tabs = [
|
||||
{
|
||||
name: :login,
|
||||
path: admin_settings_authentication_path(tab: :login),
|
||||
label: :"settings.authentication.login_and_sso",
|
||||
partial: "admin/settings/authentication_settings/login"
|
||||
},
|
||||
{
|
||||
name: :registration,
|
||||
path: admin_settings_authentication_path(tab: :registration),
|
||||
label: :"settings.authentication.registration",
|
||||
partial: "admin/settings/authentication_settings/registration"
|
||||
},
|
||||
{
|
||||
name: :passwords,
|
||||
path: admin_settings_authentication_path(tab: :passwords),
|
||||
label: :"settings.passwords",
|
||||
partial: "admin/settings/authentication_settings/passwords"
|
||||
}
|
||||
]
|
||||
%>
|
||||
|
||||
<%=
|
||||
render Primer::OpenProject::PageHeader.new do |header|
|
||||
header.with_title { t(:label_authentication_settings) }
|
||||
header.with_title { t("authentication.login_and_registration") }
|
||||
header.with_breadcrumbs(
|
||||
[{ href: admin_index_path, text: t(:label_administration) },
|
||||
{ href: admin_settings_authentication_path, text: t(:label_authentication) },
|
||||
t(:label_authentication_settings)]
|
||||
t("authentication.login_and_registration")]
|
||||
)
|
||||
render_tab_header_nav(header, tabs)
|
||||
end
|
||||
%>
|
||||
|
||||
<%= styled_form_tag(admin_settings_authentication_path, method: :patch) do %>
|
||||
<section class="form--section">
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("settings.general") %></legend>
|
||||
<div class="form--field"><%= setting_check_box :login_required %></div>
|
||||
|
||||
<div class="form--field">
|
||||
<%= setting_select :self_registration, [[t(:label_disabled), Setting::SelfRegistration.disabled.to_s],
|
||||
[t(:label_registration_activation_by_email), Setting::SelfRegistration.by_email.to_s],
|
||||
[t(:label_registration_manual_activation), Setting::SelfRegistration.manual.to_s],
|
||||
[t(:label_registration_automatic_activation), Setting::SelfRegistration.automatic.to_s]],
|
||||
container_class: "-middle" %>
|
||||
<div class="form--field-instructions">
|
||||
<% if Setting::SelfRegistration.unsupervised_registration? %>
|
||||
<%=
|
||||
render Primer::Alpha::Banner.new(
|
||||
scheme: :warning,
|
||||
icon: :alert,
|
||||
my: 2
|
||||
) do
|
||||
I18n.t(:setting_self_registration_warning)
|
||||
end
|
||||
%>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form--field">
|
||||
<%= setting_check_box :email_login, title: I18n.t("tooltip.setting_email_login") %>
|
||||
</div>
|
||||
|
||||
<%= render Settings::NumericSettingComponent.new("invitation_expiration_days", unit: "days") %>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t(:"settings.authentication.single_sign_on") %></legend>
|
||||
<div class="form--field">
|
||||
<% providers = AuthProvider
|
||||
.where(available: true)
|
||||
.order("lower(display_name) ASC")
|
||||
.select(:type, :display_name, :slug)
|
||||
.to_a
|
||||
.map { |p| ["#{p.display_name} (#{p.human_type})", p.slug] } %>
|
||||
<%= setting_select :omniauth_direct_login_provider,
|
||||
[[t(:label_disabled), ""]] + providers,
|
||||
container_class: "-middle" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= t(
|
||||
"settings.authentication.omniauth_direct_login_hint_html",
|
||||
internal_path: internal_signin_url
|
||||
) %>
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form--fieldset">
|
||||
<fieldset id="registration_footer" class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t(:setting_registration_footer) %></legend>
|
||||
<%= render Settings::TextSettingComponent.new(I18n.locale, name: "registration_footer") %>
|
||||
</fieldset>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("settings.passwords") %></legend>
|
||||
<% if !OpenProject::Configuration.disable_password_login? %>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :password_min_length, size: 6, container_class: "-xslim" %></div>
|
||||
<div class="form--field -wide-label">
|
||||
<% rules = OpenProject::Passwords::Evaluator.known_rules.map do |rule|
|
||||
[t("label_password_rule_#{rule}"), rule]
|
||||
end %>
|
||||
<%= setting_multiselect :password_active_rules, rules %>
|
||||
</div>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :password_min_adhered_rules, size: 6, container_class: "-xslim" %></div>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :password_days_valid, size: 6, container_class: "-xslim" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= t(:text_hint_disable_with_0) %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :password_count_former_banned, size: 6, container_class: "-xslim" %></div>
|
||||
<div class="form--field -wide-label"><%= setting_check_box :lost_password, label: :label_password_lost %></div>
|
||||
<% else %>
|
||||
<div class="form--field -wide-label">
|
||||
<label><b><%= I18n.t :note %>: </b>
|
||||
<%=
|
||||
url = "https://www.openproject.org/docs/installation-and-operations/configuration/#disable-password-login"
|
||||
|
||||
explanation = I18n.t :note_password_login_disabled,
|
||||
configuration: "<a target=\"_blank\" href=\"#{url}\"> #{I18n.t('label_configuration')}</a>"
|
||||
|
||||
explanation.html_safe
|
||||
%>
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
</fieldset>
|
||||
|
||||
<% unless OpenProject::Configuration.disable_password_login? %>
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("settings.brute_force_prevention") %></legend>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :brute_force_block_after_failed_logins, container_class: "-xslim" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= t(:text_hint_disable_with_0) %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form--field -wide-label"><%= setting_text_field :brute_force_block_minutes, unit: t(:label_minute_plural), container_class: "-xslim" %></div>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("settings.session") %></legend>
|
||||
<div class="form--field -wide-label"><%= setting_select :autologin,
|
||||
([[t(:label_disabled), 0]] +
|
||||
Settings::Definition[:autologin].allowed.collect do |days|
|
||||
[t("datetime.distance_in_words.x_days", count: days),
|
||||
days.to_s]
|
||||
end),
|
||||
container_class: "-xslim" %>
|
||||
</div>
|
||||
<div class="form--field -wide-label"><%= setting_check_box :session_ttl_enabled %></div>
|
||||
<div class="form--field -wide-label" id="settings_session_ttl_container" style="display:none;">
|
||||
<%= setting_text_field :session_ttl, unit: t(:label_minute_plural), container_class: "-xslim" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= I18n.t("setting_session_ttl_hint") %>
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form--fieldset">
|
||||
<legend class="form--fieldset-legend"><%= I18n.t("settings.other") %></legend>
|
||||
<div class="form--field -wide-label"><%= setting_check_box :log_requesting_user %></div>
|
||||
|
||||
<div class="form--field -wide-label">
|
||||
<%= setting_text_field :after_first_login_redirect_url, container_class: "-middle" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= t(:setting_after_first_login_redirect_url_text_html) %>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form--field -wide-label">
|
||||
<%= setting_text_field :after_login_default_redirect_url, container_class: "-middle" %>
|
||||
<span class="form--field-instructions">
|
||||
<%= t(:setting_after_login_default_redirect_url_text_html) %>
|
||||
</span>
|
||||
</div>
|
||||
</fieldset>
|
||||
</section>
|
||||
<% unless OpenProject::Configuration.disable_password_login? %>
|
||||
<div style="float:right;">
|
||||
<%= link_to t(:label_ldap_authentication), { controller: "/ldap_auth_sources", action: "index" }, class: "icon icon-server-key" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= styled_button_tag t(:button_save), class: "-primary -with-icon icon-checkmark" %>
|
||||
<% end %>
|
||||
<%=
|
||||
selected_tab = selected_tab(tabs)
|
||||
render partial: "common/tabs", locals: { tabs:, selected_tab:, with_tab_nav: false }
|
||||
%>
|
||||
|
||||
@@ -33,6 +33,6 @@ See COPYRIGHT and LICENSE files for more details.
|
||||
<% end %>
|
||||
|
||||
<%= content_tag "div",
|
||||
render(partial: selected_tab[:partial], locals: { f: f, tab: selected_tab }),
|
||||
render(partial: selected_tab[:partial], locals: local_assigns.merge(tab: selected_tab)),
|
||||
id: "tab-content-#{selected_tab[:name]}",
|
||||
class: "tab-content" %>
|
||||
|
||||
@@ -979,7 +979,8 @@ module Settings
|
||||
default: nil
|
||||
},
|
||||
self_registration: {
|
||||
default: 2
|
||||
default: 2,
|
||||
format: :integer
|
||||
},
|
||||
sendmail_arguments: {
|
||||
description: "Arguments to call sendmail with in case it is configured as outgoing email setup",
|
||||
|
||||
@@ -502,7 +502,7 @@ Redmine::MenuManager.map :admin_menu do |menu|
|
||||
menu.push :authentication_settings,
|
||||
{ controller: "/admin/settings/authentication_settings", action: :show },
|
||||
if: ->(_) { User.current.admin? },
|
||||
caption: :label_authentication_settings,
|
||||
caption: :"authentication.login_and_registration",
|
||||
parent: :authentication
|
||||
|
||||
menu.push :ldap_authentication,
|
||||
|
||||
+29
-4
@@ -112,6 +112,9 @@ en:
|
||||
text: "Individual actions of a user (e.g. updating a work package twice) are aggregated into a single action if their age difference is less than the specified timespan. They will be displayed as a single action within the application. This will also delay notifications by the same amount of time reducing the number of emails being sent and will also affect %{webhook_link} delay."
|
||||
link: "webhook"
|
||||
|
||||
authentication:
|
||||
login_and_registration: "Login and registration"
|
||||
|
||||
announcements:
|
||||
show_until: Show until
|
||||
is_active: currently displayed
|
||||
@@ -2571,7 +2574,6 @@ en:
|
||||
label_ldap_auth_source_plural: "LDAP connections"
|
||||
label_attribute_expand_text: "The complete text for '%{attribute}'"
|
||||
label_authentication: "Authentication"
|
||||
label_authentication_settings: "Authentication settings"
|
||||
label_available_custom_fields_projects: "Available custom fields projects"
|
||||
label_available_global_roles: "Available global roles"
|
||||
label_available_project_attributes: "Available project attributes"
|
||||
@@ -2974,9 +2976,6 @@ en:
|
||||
label_register: "Create a new account"
|
||||
label_register_with_developer: "Register as developer"
|
||||
label_registered_on: "Registered on"
|
||||
label_registration_activation_by_email: "account activation by email"
|
||||
label_registration_automatic_activation: "automatic account activation"
|
||||
label_registration_manual_activation: "manual account activation"
|
||||
label_related_work_packages: "Related work packages"
|
||||
label_relates: "related to"
|
||||
label_relates_to: "related to"
|
||||
@@ -3864,6 +3863,7 @@ en:
|
||||
This defines what is considered a "day" when displaying duration in days and hours
|
||||
(for example, if a day is 8 hours, 32 hours would be 4 days).
|
||||
setting_invitation_expiration_days: "Activation email expires after"
|
||||
setting_invitation_expiration_days_caption: "Number of days after which the activation email expires."
|
||||
setting_work_package_done_ratio: "Progress calculation mode"
|
||||
setting_work_package_done_ratio_field: "Work-based"
|
||||
setting_work_package_done_ratio_field_caption_html: >-
|
||||
@@ -3883,6 +3883,9 @@ en:
|
||||
setting_journal_aggregation_time_minutes: "User actions aggregated within"
|
||||
setting_log_requesting_user: "Log user login, name, and mail address for all requests"
|
||||
setting_login_required: "Authentication required"
|
||||
setting_login_required_caption: "When checked, all requests to the application have to be authenticated."
|
||||
setting_lost_password: "Enable password reset"
|
||||
setting_lost_password_caption: "When checked, allow users to reset their own passwords."
|
||||
setting_mail_from: "Emission email address"
|
||||
setting_mail_handler_api_key: "API key"
|
||||
setting_mail_handler_body_delimiters: "Truncate emails after one of these lines"
|
||||
@@ -3908,6 +3911,7 @@ en:
|
||||
setting_project_gantt_query_text: "You can modify the query that is used to display Gantt chart from the project overview page."
|
||||
setting_security_badge_displayed: "Display security badge"
|
||||
setting_registration_footer: "Registration footer"
|
||||
setting_registration_footer_caption: "This text is displayed in the footer of the registration page. Use the HTML editor to format the text for each selected language."
|
||||
setting_repositories_automatic_managed_vendor: "Automatic repository vendor type"
|
||||
setting_repositories_encodings: "Repositories encodings"
|
||||
setting_repository_storage_cache_minutes: "Repository disk size cache"
|
||||
@@ -3918,10 +3922,29 @@ en:
|
||||
setting_repository_truncate_at: "Maximum number of files displayed in the repository browser"
|
||||
setting_rest_api_enabled: "Enable REST web service"
|
||||
setting_self_registration: "Self-registration"
|
||||
setting_self_registration_caption: >
|
||||
Choose the self-registration mechanism for users. Be careful with the setting you choose, as some
|
||||
options allow users to activate their own accounts to this instance.
|
||||
setting_self_registration_warning: >
|
||||
The user will be able to activate their own accounts.
|
||||
Please note that this will give them access to all public projects and their content.
|
||||
Please make sure that no sensitive or private data is exposed in public projects.
|
||||
setting_self_registration_disabled: "Disabled"
|
||||
setting_self_registration_disabled_caption: >
|
||||
No accounts can be registered on their own. Only administrators and users with the global permission
|
||||
to create new users are able to create new accounts.
|
||||
setting_self_registration_activation_by_email: "Account activation by email"
|
||||
setting_self_registration_activation_by_email_caption: >
|
||||
Users can register on their own and activate their account after confirming their email address.
|
||||
Administrators have no moderation control over the activation process.
|
||||
setting_self_registration_automatic_activation: "Automatic account activation"
|
||||
setting_self_registration_automatic_activation_caption: >
|
||||
Users can register on their own. Their accounts are immediately active without further action.
|
||||
Administrators have no moderation control over the activation process.
|
||||
setting_self_registration_manual_activation: "Manual account activation"
|
||||
setting_self_registration_manual_activation_caption: >
|
||||
Users can register on their own. Their accounts are in a pending state until an administrator
|
||||
or user with the global permission to create or manage users activates them.
|
||||
setting_session_ttl: "Session expiry time after inactivity"
|
||||
setting_session_ttl_hint: "Value below 5 works like disabled"
|
||||
setting_session_ttl_enabled: "Session expires"
|
||||
@@ -3951,6 +3974,8 @@ en:
|
||||
|
||||
settings:
|
||||
authentication:
|
||||
login_and_sso: "Login and SSO"
|
||||
registration: "Registration"
|
||||
single_sign_on: "Single Sign-On"
|
||||
omniauth_direct_login_hint_html: >
|
||||
If this option is active, login requests will redirect to the configured omniauth provider.
|
||||
|
||||
@@ -185,3 +185,5 @@ ical_docs:
|
||||
href: https://www.openproject.org/docs/user-guide/calendar/#subscribe-to-a-calendar
|
||||
integrations:
|
||||
href: https://www.openproject.org/docs/system-admin-guide/integrations/
|
||||
disable_password_login:
|
||||
href: https://www.openproject.org/docs/installation-and-operations/configuration/#disable-password-login
|
||||
|
||||
@@ -32,9 +32,10 @@ export function listenToSettingChanges() {
|
||||
const id:string = self.attr('id') || '';
|
||||
const settingName = id.replace('lang-for-', '');
|
||||
const newLang = self.val() as string;
|
||||
const textArea = jQuery(`#settings-${settingName}`);
|
||||
const textAreaId = `#settings-${settingName}`;
|
||||
const textArea = jQuery(textAreaId);
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-assignment */
|
||||
const editor = textArea.siblings('opce-ckeditor-augmented-textarea').data('editor');
|
||||
const editor = jQuery(`opce-ckeditor-augmented-textarea[data-textarea-selector='"${textAreaId}"'`).data('editor');
|
||||
|
||||
return {
|
||||
id, settingName, newLang, textArea, editor,
|
||||
|
||||
+5
-4
@@ -66,7 +66,7 @@ import { uniqueId } from 'lodash';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin implements OnInit {
|
||||
@Input() public textareaSelector:string;
|
||||
@Input() public textAreaId:string;
|
||||
|
||||
@Input() public previewContext:string;
|
||||
|
||||
@@ -144,7 +144,9 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl
|
||||
this.halResource = this.resource ? this.halResourceService.createHalResource(this.resource, true) : undefined;
|
||||
|
||||
this.formElement = this.element.closest<HTMLFormElement>('form') as HTMLFormElement;
|
||||
this.wrappedTextArea = this.formElement.querySelector(this.textareaSelector) as HTMLTextAreaElement;
|
||||
|
||||
this.wrappedTextArea = document.getElementById(this.textAreaId) as HTMLTextAreaElement;
|
||||
|
||||
this.wrappedTextArea.style.display = 'none';
|
||||
this.wrappedTextArea.required = false;
|
||||
this.initialContent = this.wrappedTextArea.value;
|
||||
@@ -286,8 +288,7 @@ export class CkeditorAugmentedTextareaComponent extends UntilDestroyedMixin impl
|
||||
}
|
||||
|
||||
private setLabel() {
|
||||
const textareaId = this.textareaSelector.substring(1);
|
||||
const label = document.querySelector<HTMLLabelElement>(`label[for=${textareaId}]`)!;
|
||||
const label = document.querySelector<HTMLLabelElement>(`label[for=${this.textAreaId}]`)!;
|
||||
|
||||
const ckContent = this.element.querySelector<HTMLElement>('.ck-content')!;
|
||||
|
||||
|
||||
@@ -73,6 +73,10 @@ module OpenProject
|
||||
Setting.password_active_rules
|
||||
end
|
||||
|
||||
def self.active_rule?(rule)
|
||||
Setting.password_active_rules.include?(rule.to_s)
|
||||
end
|
||||
|
||||
# Checks whether password adheres to complexity rules.
|
||||
# Does not check length.
|
||||
def self.password_conforms_to_rules(password)
|
||||
|
||||
@@ -51,7 +51,7 @@ module OpenProject::TextFormatting::Formats
|
||||
resource = context.fetch(:resource, {})
|
||||
helpers.angular_component_tag "opce-ckeditor-augmented-textarea",
|
||||
inputs: {
|
||||
textareaSelector: "##{field_id}",
|
||||
textAreaId: field_id,
|
||||
editorType: context[:editor_type] || "full",
|
||||
previewContext: context[:preview_context],
|
||||
resource:,
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<%= render(FormControl.new(input: @input)) do %>
|
||||
<%= content_tag(:div, hidden: true) do %>
|
||||
<%= builder.text_area(@input.name, **@input.input_arguments) %>
|
||||
<%= builder.text_area(@input.name,
|
||||
id: @text_area_id,
|
||||
**@input.input_arguments) %>
|
||||
<% end %>
|
||||
<%= angular_component_tag "opce-ckeditor-augmented-textarea",
|
||||
inputs: @rich_text_options.reverse_merge(
|
||||
{
|
||||
textareaSelector: "##{builder.field_id(@input.name)}",
|
||||
textAreaId: @text_area_id,
|
||||
macros: false,
|
||||
turboMode: true
|
||||
}
|
||||
|
||||
@@ -13,7 +13,9 @@ module Primer
|
||||
super()
|
||||
@input = input
|
||||
@rich_text_data = rich_text_options.delete(:data) { {} }
|
||||
@rich_text_data[:"test-selector"] ||= "augmented-text-area-#{@input.name}"
|
||||
@rich_text_options = rich_text_options
|
||||
@text_area_id = rich_text_options.delete(:text_area_id) || builder.field_id(@input.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -507,7 +507,7 @@ end
|
||||
|
||||
It is easier to write and read.
|
||||
|
||||
Under the hood, the form object is decorated with `SettingsFormDecorator`.
|
||||
Under the hood, the form object is decorated with `Settings::FormDecorator`.
|
||||
That's where all the helper methods are defined. There aren't many for now, but
|
||||
this is intended to grow to support more advanced form features for
|
||||
administration pages.
|
||||
|
||||
@@ -60,7 +60,7 @@ RSpec.describe Projects::Settings::General::ShowComponent, type: :component do
|
||||
it "renders fields" do
|
||||
expect(render_component).to have_field "Name", required: true
|
||||
expect(render_component).to have_element "opce-ckeditor-augmented-textarea",
|
||||
"data-textarea-selector": "\"#project_description\""
|
||||
"data-test-selector": "augmented-text-area-description"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -70,8 +70,9 @@ RSpec.describe Projects::Settings::General::ShowComponent, type: :component do
|
||||
it "renders fields" do
|
||||
expect(render_component).to have_element "opce-autocompleter",
|
||||
"data-input-name": "\"project[status_code]\""
|
||||
|
||||
expect(render_component).to have_element "opce-ckeditor-augmented-textarea",
|
||||
"data-textarea-selector": "\"#project_status_explanation\""
|
||||
"data-test-selector": "augmented-text-area-description"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -113,23 +113,22 @@ RSpec.describe "random password generation", :js do
|
||||
end
|
||||
|
||||
it "can configure and enforce password rules" do
|
||||
visit admin_settings_authentication_path
|
||||
expect_angular_frontend_initialized
|
||||
visit admin_settings_authentication_path(tab: :passwords)
|
||||
|
||||
# Enforce rules
|
||||
# 3 of 'lowercase, uppercase, special'
|
||||
find(".form--check-box[value=uppercase]").set true
|
||||
find(".form--check-box[value=lowercase]").set true
|
||||
find(".form--check-box[value=numeric]").set false
|
||||
find(".form--check-box[value=special]").set true
|
||||
check "Lowercase"
|
||||
check "Uppercase"
|
||||
check "Special"
|
||||
uncheck "Numeric"
|
||||
|
||||
# Set min length to 4
|
||||
find_by_id("settings_password_min_length").set 4
|
||||
fill_in "Minimum length", with: 4
|
||||
|
||||
# Set min classes to 3
|
||||
find_by_id("settings_password_min_adhered_rules").set 3
|
||||
fill_in "Minimum number of required classes", with: 3
|
||||
|
||||
scroll_to_and_click(find(".button", text: "Save"))
|
||||
click_on "Save"
|
||||
expect_flash(message: "Successful update.")
|
||||
|
||||
Setting.clear_cache
|
||||
|
||||
@@ -38,7 +38,6 @@ RSpec.describe Projects::Settings::DescriptionForm, type: :forms do
|
||||
it "renders field" do
|
||||
expect(page).to have_field "Description", with: "example description", visible: :hidden
|
||||
expect(page).to have_element "opce-ckeditor-augmented-textarea",
|
||||
"data-textarea-selector": "\"#project_description\""
|
||||
expect(page).to have_element "opce-ckeditor-augmented-textarea", "data-qa-field-name": "description"
|
||||
"data-test-selector": "augmented-text-area-description"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -51,7 +51,7 @@ RSpec.describe Projects::Settings::StatusForm, type: :forms do
|
||||
it "renders status description field" do
|
||||
expect(page).to have_field "Project status description", with: "example status info", visible: :hidden
|
||||
expect(page).to have_element "opce-ckeditor-augmented-textarea",
|
||||
"data-textarea-selector": "\"#project_status_explanation\""
|
||||
"data-test-selector": "augmented-text-area-status_explanation"
|
||||
expect(page).to have_element "opce-ckeditor-augmented-textarea", "data-qa-field-name": "statusExplanation"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
#-- copyright
|
||||
# OpenProject is an open source project management software.
|
||||
# Copyright (C) the OpenProject GmbH
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License version 3.
|
||||
#
|
||||
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
|
||||
# Copyright (C) 2006-2013 Jean-Philippe Lang
|
||||
# Copyright (C) 2010-2013 the ChiliProject Team
|
||||
#
|
||||
# This program is free software; you can redistribute it and/or
|
||||
# modify it under the terms of the GNU General Public License
|
||||
# as published by the Free Software Foundation; either version 2
|
||||
# of the License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program; if not, write to the Free Software
|
||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||
#
|
||||
# See COPYRIGHT and LICENSE files for more details.
|
||||
#++
|
||||
|
||||
require "spec_helper"
|
||||
|
||||
RSpec.describe "Authentication Settings",
|
||||
:skip_csrf,
|
||||
type: :rails_request do
|
||||
let(:admin) { create(:admin) }
|
||||
|
||||
before do
|
||||
login_as(admin)
|
||||
end
|
||||
|
||||
describe "GET /admin/settings/authentication?tab=passwords" do
|
||||
context "with password login enabled" do
|
||||
before do
|
||||
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(false)
|
||||
get "/admin/settings/authentication.html?tab=passwords"
|
||||
end
|
||||
|
||||
it "shows password settings" do
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(page).to have_field(I18n.t(:setting_lost_password), disabled: false)
|
||||
expect(page).to have_field(I18n.t(:setting_brute_force_block_after_failed_logins), disabled: false)
|
||||
end
|
||||
end
|
||||
|
||||
context "with password login disabled" do
|
||||
before do
|
||||
allow(OpenProject::Configuration).to receive(:disable_password_login?).and_return(true)
|
||||
get "/admin/settings/authentication.html?tab=passwords"
|
||||
end
|
||||
|
||||
it "disables password settings" do
|
||||
expect(response).to have_http_status(:success)
|
||||
|
||||
expect(page).to have_field(I18n.t(:setting_lost_password), disabled: true)
|
||||
expect(page).to have_field(I18n.t(:setting_brute_force_block_after_failed_logins), disabled: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -14,7 +14,7 @@ module FormFields
|
||||
end
|
||||
|
||||
def field_container
|
||||
augmented_textarea = page.find("[data-textarea-selector='\"#project_custom_field_values_#{property.id}\"']")
|
||||
augmented_textarea = page.find("[data-text-area-id='\"project_custom_field_values_#{property.id}\"']")
|
||||
augmented_textarea.first(:xpath, ".//..")
|
||||
end
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ module Pages::Admin::SystemSettings
|
||||
end
|
||||
|
||||
def welcome_text_selector
|
||||
'opce-ckeditor-augmented-textarea[data-textarea-selector="\"#settings_welcome_text\""]'
|
||||
'opce-ckeditor-augmented-textarea[data-text-area-id="\"settings_welcome_text\""]'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user