diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27a203d..11c0b0e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: --health-retries=5 env: - REDMINE_VER: 5.1-stable + REDMINE_VER: 6.1-stable PLUGIN_NAME: vault REDMINE_GIT_REPO: https://github.com/redmine/redmine.git REDMINE_PATH: ${{ github.workspace }}/redmine diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c6f2d..89bbe57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,29 @@ # Changelog -## Version: 0.10.6 + +## Version: 0.11.0 ### Improvements -- Prepare for next release +- Refactored the search page to support query-based patterns +- Removed custom CSS styles to ensure compatibility with themes +- Updated form components to better match Redmine visual standards +- Adjusted UI navigation for consistency with Redmine structure +- Integrated Select2 JavaScript component for enhanced tag selection +- Added support for Redmine 6.1 +### Bugfix +- Fixed misindented `end` in `keys_controller` index action causing misleading code structure +- Fixed XSS-pattern in form view: replaced `raw` with `json_escape` for inline JSON tag data +- Fixed `_key_fields` partial depending on `@query` instance variable; now accepts a `query` local with `@query` fallback +- Fixed tag assignment in `create` action: tags are now set after save so the join table has a valid `key_id` +- Fixed `update` action passing tags through mass-assignment; tags are now handled separately +### Locales +- Fixed broken YAML structure in `ja.yml` (all keys were incorrectly nested under `activerecord`) +- Translated all remaining English strings in `ja.yml` +- Added missing `key_file` model name to `de`, `es`, `fr`, `it`, `ja`, `nl` locales +- Added missing `field_has_url`, `field_has_login`, `field_body`, `key.btn.generate` to all non-English locales +- Added missing `key.audit_log`, `key.btn.move/edit_tags`, `key.attr.project/created_at/updated_at`, `error.key.length/not_orphaned`, `error.project.required` to all non-English locales +- Fixed untranslated strings in `de`, `fr`, `it`, `nl`, `zh` locales +- Added `permission_keys_all` to `ru` locale +### Tests +- Added `test/unit/locale_test.rb` to assert all locales contain every key defined in `en.yml` ## Version: 0.10.5 ### Bugfix @@ -51,10 +73,10 @@ ### Bugfix - [During view/edit key no link to download file](https://github.com/noshutdown-ru/vault/issues/92) - [Json format export fixed to Redmine standard](https://github.com/noshutdown-ru/vault/issues/90) -- Fixed broken filter by tags -- Translation fixes +- Fixed broken filter by tags +- Translation fixes -## Version: 0.7.3 +## Version: 0.7.3 ### Bugfix - [Fixing error viewing Key File types](https://github.com/noshutdown-ru/vault/issues/110) @@ -97,7 +119,7 @@ - Added Github Actions for CI ### Braking changes -- Deleted code which checks Redmine version +- Deleted code which checks Redmine version - `Redmine::VERSION.to_s.start_with?` - 3.1/3.2/3.3/3.4/4 @@ -127,14 +149,14 @@ - Added French translation. - [Import from backup update existing keys by name instead of create new ones.](https://github.com/noshutdown-ru/vault/pull/53) - [Whitelists support groups.](https://github.com/noshutdown-ru/vault/pull/51) -### Bugfixes +### Bugfixes - [Export keys not working on Windows.](https://github.com/noshutdown-ru/vault/pull/52) - [Error in redmine subdir icons display.](https://github.com/noshutdown-ru/vault/pull/47) ## Version: 0.3.11 ### Improvements - [Support Redmine 4.0.* .](https://github.com/noshutdown-ru/vault/pull/45) -### Bugfixes +### Bugfixes - [Menu admin no icon.](https://github.com/noshutdown-ru/vault/issues/46) ## Version: 0.3.10 @@ -157,11 +179,11 @@ - [Copy to clipboard.](https://github.com/noshutdown-ru/vault/issues/28) ## Version: 0.3.7 -### Bugfixes +### Bugfixes - [Search not working.](https://github.com/noshutdown-ru/vault/issues/24) ## Version: 0.3.6 -### Bugfixes +### Bugfixes - [Undefined method 'offset'.](https://github.com/noshutdown-ru/vault/issues/23) ## Version: 0.3.5 @@ -169,7 +191,7 @@ - [White lists not block user by direct link.](https://github.com/noshutdown-ru/vault/issues/22) ## Version: 0.3.4 -- [Error on searching by Name/URL (PostgreSQL).](https://github.com/noshutdown-ru/vault/issues/13) +- [Error on searching by Name/URL (PostgreSQL).](https://github.com/noshutdown-ru/vault/issues/13) - [Right click no url (Redmine 3.4).](https://github.com/noshutdown-ru/vault/issues/17) ## Version: 0.3.3 @@ -180,7 +202,7 @@ ### Features - Added support Redmine 3.4 . - Added copy by click on the fields: url, login. -- Added China translation. +- Added China translation. - Added Dutch translation. - Added Italian translation. ### Bugfixes diff --git a/app/controllers/keys_controller.rb b/app/controllers/keys_controller.rb index cdfac3e..cc37631 100755 --- a/app/controllers/keys_controller.rb +++ b/app/controllers/keys_controller.rb @@ -7,6 +7,8 @@ class KeysController < ApplicationController helper :sort include SortHelper + helper :queries + include QueriesHelper def index unless Setting.plugin_vault['use_redmine_encryption'] || @@ -17,42 +19,45 @@ def index end end - sort_init 'name', 'asc' - sort_update 'name' => "#{Vault::Key.table_name}.name" + retrieve_query(Vault::KeyQuery) + sort_init(@query.sort_criteria.empty? ? [['name', 'asc']] : @query.sort_criteria) + sort_update(@query.sortable_columns) + @query.sort_criteria = sort_criteria.to_a + @search = params[:search].to_s - @keys = @project.keys - @keys = @keys.order(sort_clause) - @keys = @keys.select { |key| key.whitelisted?(User.current, @project) } - @keys = [] if @keys.nil? # hack for decryption + if @query.valid? + @limit = per_page_option - # Filter by tag if query parameter contains #tagname - @query = params[:query] - if @query && !@query.empty? && @query.match(/#/) - tag_string = (@query.match(/(#)([^,]+)/))[2] - tag = Vault::Tag.find_by_name(tag_string) - @keys = tag.nil? ? [] : @keys.select { |key| key.tags.include?(tag) } - end + scoped_keys = @query.results_scope( + search: @search, + order: sort_clause + ) - @limit = per_page_option - @key_count = @keys.count - @key_pages = Paginator.new @key_count, @limit, params[:page] - @offset ||= @key_pages.offset + all_visible_keys = scoped_keys.to_a.select { |key| key.whitelisted?(User.current, @project) } + @key_count = all_visible_keys.size + @key_pages = Paginator.new(@key_count, @limit, params[:page]) + @offset ||= @key_pages.offset + @keys = all_visible_keys.drop(@offset).first(@limit) + @keys.each(&:decrypt!) - if @key_count > 0 - @keys = @keys.drop(@offset).first(@limit) - end - - @keys.map(&:decrypt!) - - respond_to do |format| - format.html - format.pdf do - unless User.current.allowed_to?(:export_keys, @project) - render_error t("error.user.not_allowed") - return + respond_to do |format| + format.html do + render partial: 'list', layout: false if request.xhr? + end + format.pdf do + unless User.current.allowed_to?(:export_keys, @project) + render_error t("error.user.not_allowed") + return + end end + format.json { render json: { keys: @keys } } + end + else + respond_to do |format| + format.html { render template: 'keys/index', layout: !request.xhr? } + format.any(:pdf) { render plain: '' } + format.json { render_validation_errors(@query) } end - format.json { render json: { keys: @keys } } end end @@ -128,7 +133,6 @@ def create save_file if key_params[:file] @key = Vault::Key.new @key.safe_attributes = key_params.except(:tags) - @key.tags = key_params[:tags] @key.project = @project @key.audit_user = User.current @@ -136,6 +140,7 @@ def create respond_to do |format| if @key.save + @key.tags = key_params[:tags] format.html { redirect_to project_keys_path(@project), notice: t('notice.key.create.success') } format.json { render json: { key: @key }, status: :created, location: project_key_path(@project, @key) } else @@ -152,7 +157,7 @@ def update @key.safe_attributes = key_params.except(:tags) @key.audit_user = User.current - if @key.update(key_params) + if @key.update(key_params.except(:tags)) @key.tags = key_params[:tags] format.html { redirect_to project_keys_path(@project), notice: t('notice.key.update.success') } format.json { render json: { key: @key }, status: :ok } diff --git a/app/controllers/vault_settings_controller.rb b/app/controllers/vault_settings_controller.rb index d9b6754..80bdf7f 100644 --- a/app/controllers/vault_settings_controller.rb +++ b/app/controllers/vault_settings_controller.rb @@ -19,10 +19,10 @@ def save # Check if encryption setting is changing old_encrypt_files = Setting.plugin_vault['encrypt_files'] - new_encrypt_files = params[:settings][:encrypt_files] + new_encrypt_files = settings['encrypt_files'] # Save settings first - Setting.send "plugin_vault=", params[:settings] + Setting.send "plugin_vault=", settings # Handle encryption state change if old_encrypt_files != new_encrypt_files diff --git a/app/models/vault/key_query.rb b/app/models/vault/key_query.rb new file mode 100644 index 0000000..fb9134a --- /dev/null +++ b/app/models/vault/key_query.rb @@ -0,0 +1,167 @@ +module Vault + class KeyQuery < Query + self.queried_class = Vault::Key + self.view_permission = :view_keys + + self.available_columns = [ + QueryColumn.new(:type, sortable: "#{Vault::Key.table_name}.type"), + QueryColumn.new(:name, sortable: "#{Vault::Key.table_name}.name"), + QueryColumn.new(:url, sortable: "#{Vault::Key.table_name}.url"), + QueryColumn.new(:login, sortable: "#{Vault::Key.table_name}.login"), + QueryColumn.new(:body), + ] + + def initialize(attributes = nil, *args) + super(attributes) + self.filters ||= {} + end + + def initialize_available_filters + add_available_filter( + 'type', + type: :list_optional, + values: [ + [::I18n.t('activerecord.models.password'), 'Vault::Password'], + [::I18n.t('activerecord.models.sftp'), 'Vault::Sftp'], + [::I18n.t('activerecord.models.key_file'), 'Vault::KeyFile'] + ] + ) + + add_available_filter('name', type: :string) + add_available_filter('url', type: :string) + add_available_filter('login', type: :string) + + add_available_filter( + 'tags', + type: :list_optional, + values: available_tag_values + ) + + add_available_filter( + 'has_url', + type: :list, + values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']] + ) + + add_available_filter( + 'has_login', + type: :list, + values: [[l(:general_text_yes), '1'], [l(:general_text_no), '0']] + ) + end + + def default_columns_names + @default_columns_names ||= [:type, :name, :url, :login, :body] + end + + def default_sort_criteria + [['name', 'asc']] + end + + def base_scope + scope = queried_class.joins(:project).where(project_id: project&.id) + scope = scope.where(statement) if statement.present? + scope + end + + def results_scope(options = {}) + scope = base_scope + scope = apply_live_search(scope, options[:search]) + + order_option = [group_by_sort_order, (options[:order] || sort_clause)].flatten.reject(&:blank?) + order_option << "#{Vault::Key.table_name}.id ASC" + + scope.order(order_option).joins(joins_for_order_statement(order_option.join(','))).distinct + end + + def sql_for_tags_field(_field, operator, value) + connection = ActiveRecord::Base.connection + keys_table = connection.quote_table_name(Vault::Key.table_name) + tags_table = connection.quote_table_name(Vault::Tag.table_name) + join_table = connection.quote_table_name('keys_vault_tags') + ids_column = "#{keys_table}.#{connection.quote_column_name('id')}" + key_id_column = "#{join_table}.#{connection.quote_column_name('key_id')}" + tag_id_column = "#{join_table}.#{connection.quote_column_name('tag_id')}" + tag_name_column = "#{tags_table}.#{connection.quote_column_name('name')}" + + case operator + when '*' + "#{ids_column} IN (SELECT DISTINCT #{key_id_column} FROM #{join_table})" + when '!*' + "#{ids_column} NOT IN (SELECT DISTINCT #{key_id_column} FROM #{join_table})" + else + values = Array(value).reject(&:blank?) + return operator == '!' ? '1=1' : '1=0' if values.empty? + + quoted_values = values.map { |v| connection.quote(v) } + key_ids_subquery = "SELECT DISTINCT #{ids_column} FROM #{keys_table} " \ + "INNER JOIN #{join_table} ON #{key_id_column} = #{ids_column} " \ + "INNER JOIN #{tags_table} ON #{tags_table}.#{connection.quote_column_name('id')} = #{tag_id_column} " \ + "WHERE #{tag_name_column} IN (#{quoted_values.join(',')})" + + if operator == '!' + "#{ids_column} NOT IN (#{key_ids_subquery})" + else + "#{ids_column} IN (#{key_ids_subquery})" + end + end + end + + def sql_for_has_url_field(_field, operator, value) + sql_for_presence_field('url', operator, value) + end + + def sql_for_has_login_field(_field, operator, value) + sql_for_presence_field('login', operator, value) + end + + def sql_for_has_body_field(_field, operator, value) + sql_for_presence_field('body', operator, value) + end + + private + + def available_tag_values + return [] unless project + + Vault::Tag.cloud_for_project(project.id).map { |tag_name| [tag_name, tag_name] } + end + + def apply_live_search(scope, raw_search) + search = raw_search.to_s.strip + return scope if search.blank? + + search.split(/\s+/).reject(&:blank?).each do |token| + if token.start_with?('#') + tag_name = token.delete_prefix('#') + next if tag_name.blank? + + scope = scope.joins(:tags).where("#{Vault::Tag.table_name}.name = ?", tag_name) + else + pattern = "%#{queried_class.sanitize_sql_like(token)}%" + scope = scope.where( + "#{Vault::Key.table_name}.name LIKE :q OR #{Vault::Key.table_name}.login LIKE :q OR #{Vault::Key.table_name}.url LIKE :q OR #{Vault::Key.table_name}.body LIKE :q", + q: pattern + ) + end + end + + scope + end + + def sql_for_presence_field(column_name, operator, value) + positive = value.first.to_s == '1' + positive = !positive if exclude_operator?(operator) + + if positive + "(#{Vault::Key.table_name}.#{column_name} IS NOT NULL AND #{Vault::Key.table_name}.#{column_name} <> '')" + else + "(#{Vault::Key.table_name}.#{column_name} IS NULL OR #{Vault::Key.table_name}.#{column_name} = '')" + end + end + + def exclude_operator?(operator) + ['!', '!*'].include?(operator) + end + end +end diff --git a/app/models/vault/tag.rb b/app/models/vault/tag.rb index 5e3e9eb..82f4b83 100644 --- a/app/models/vault/tag.rb +++ b/app/models/vault/tag.rb @@ -38,6 +38,12 @@ def self.tags_list(pid) ).group('vault_tags.name').group('vault_tags.id').map(&:name) # OPTIMIZE_ME! end + def self.tags_list_with_colors(pid) + Vault::Tag.joins(:keys).where(keys: { project_id: pid }) + .group('vault_tags.id').select('vault_tags.name, vault_tags.color') + .map { |t| { name: t.name, color: t.color } } + end + def self.get_color(tag_name) tag = find_by(name: tag_name) tag.color if tag diff --git a/app/views/keys/_form.html.erb b/app/views/keys/_form.html.erb index f3f7dd6..a624c11 100644 --- a/app/views/keys/_form.html.erb +++ b/app/views/keys/_form.html.erb @@ -1,4 +1,4 @@ -<%= form_for key, url: { controller: :keys, action: action } do |f| %> + <%= form_for key, as: :vault_key, url: { controller: :keys, action: action } do |f| %>

<%= f.label :name, t('key.attr.name') %> @@ -9,6 +9,11 @@ <% end %>

+

+ <%= f.label :type, t('key.attr.type') %> + <%= f.select :type, key_types %> +

+

<%= f.label :login, t('key.attr.login') %> <% if User.current.allowed_to?(:edit_keys, @project) %> @@ -27,22 +32,25 @@ <% end %>

-

- <%= f.label :type %> - <%= f.select :type, key_types %> -

-

<%= f.label :body, t('key.attr.body') %> <% if User.current.allowed_to?(:edit_keys, @project) %> -

- <%= f.text_field :body, :autocomplete => :off %> - + -
+ <% else %> - <%= f.text_field :body, :readonly => true %> + + <%= f.password_field :body, :readonly => true, :style => 'width: 300px;', :id => 'vault_key_body', :value => key.body %> + + <% end %>

@@ -55,7 +63,7 @@ <% end %> <% end %> <% if User.current.allowed_to?(:edit_keys, @project) %> - <%= f.file_field :file, style: "display: none;" %> @@ -72,18 +80,21 @@

<%= f.label :tags, t('key.attr.tags') %> - <%= f.text_field :tags, value: Vault::Tag.tags_to_string(key.tags), class: 'autocomplete' %> - <% if @key.persisted? %> - <%= link_to t('key.btn.edit_tags'), project_key_tags_path(@project, @key), class: 'btn btn-primary' %> - <% end %> + <% project_tags = Vault::Tag.tags_list_with_colors(@project.id) %> + <%= f.text_field :tags, value: Vault::Tag.tags_to_string(key.tags), id: 'vault_key_tags', style: 'position:absolute;left:-9999px;top:-9999px;width:1px;height:1px;' %> + +

<%= f.label :comment, t('key.attr.comment') %> <% if User.current.allowed_to?(:edit_keys, @project) %> - <%= f.text_area :comment, class: 'wiki-edit' %> + <%= f.text_area :comment, class: 'wiki-edit', rows: 6 %> <% else %> - <%= f.text_area :comment, class: 'wiki-edit', :readonly => true %> + <%= f.text_area :comment, class: 'wiki-edit', rows: 6, :readonly => true %> <% end %>

@@ -128,9 +139,8 @@
<% if User.current.allowed_to?(:edit_keys, @project) %> - <%= f.submit t('button_save') %> + <%= f.submit key.new_record? ? 'Create' : t('button_save') %> <% end %> - <%= link_to t('button_back'), project_keys_path(@project) %> <% end %> <%= javascript_tag do %> @@ -161,10 +171,61 @@ $('#vault_whitelist').closest('form').submit(function(){ $('#selected_users option').prop('selected', true); }); + + // Select2 tags + function vaultTagColor(id) { + var match = $.grep(vaultProjectTags, function(t) { return t.name === id; }); + return match.length ? match[0].color : '#888888'; + } + + // Pre-populate select with current tags + $.each(vaultCurrentTags, function(i, tag) { + $('#vault_tags_select').append(new Option(tag.text, tag.id, true, true)); + }); + + $('#vault_tags_select').select2({ + tags: true, + tokenSeparators: [','], + placeholder: '', + data: $.map(vaultProjectTags, function(t) { return { id: t.name, text: t.name }; }), + templateResult: function(item) { + if (!item.id) { return item.text; } + var color = vaultTagColor(item.id); + var label = $('').text(item.text).html(); + return $('' + label + ''); + }, + templateSelection: function(item) { + var color = vaultTagColor(item.id); + var label = $('').text(item.text).html(); + return $('' + label + ''); + } + }); + + // Sync Select2 values to hidden field before submit + $('#vault_tags_select').closest('form').on('submit', function() { + var selected = $('#vault_tags_select').val() || []; + $('#vault_key_tags').val(selected.join(', ')); + }); }); + function togglePassword(fieldId, iconId) { + var field = document.getElementById(fieldId); + var icon = document.getElementById(iconId); + if (!field) return; + if (field.type === 'password') { + field.type = 'text'; + if (icon) { icon.classList.remove('fa-eye'); icon.classList.add('fa-eye-slash'); } + } else { + field.type = 'password'; + if (icon) { icon.classList.remove('fa-eye-slash'); icon.classList.add('fa-eye'); } + } + } + function generatePassword() { - const passwordField = document.getElementById('vault_key_body'); + const passwordField = document.getElementById('vault_key_body') || document.querySelector('input[name$="[body]"]'); + if (!passwordField) { + return; + } const currentPassword = passwordField.value.trim(); // If field is not empty, ask for confirmation diff --git a/app/views/keys/_list.html.erb b/app/views/keys/_list.html.erb new file mode 100644 index 0000000..6e2692b --- /dev/null +++ b/app/views/keys/_list.html.erb @@ -0,0 +1,19 @@ + + + + + <% @query.inline_columns.each do |column| %> + <%= column_header(@query, column) %> + <% end %> + + + + + <% (@keys || []).each do |key| %> + <%= render partial: key, locals: { parity: cycle('odd', 'even') } %> + <% end %> + +
+ <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection', + :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> + <%= t('key.btn.actions') %>
diff --git a/app/views/keys/delete.html.erb b/app/views/keys/delete.html.erb index 36624c3..ab2e33d 100644 --- a/app/views/keys/delete.html.erb +++ b/app/views/keys/delete.html.erb @@ -1,3 +1 @@

<%= t('key.title.delete') %>

- -<%= link_to t('button_back'), project_keys_path(@project) %> diff --git a/app/views/keys/edit.html.erb b/app/views/keys/edit.html.erb index c879b4c..744b639 100644 --- a/app/views/keys/edit.html.erb +++ b/app/views/keys/edit.html.erb @@ -1,707 +1,10 @@ -
- <%= form_for @key, url: { controller: :keys, action: :update }, as: :vault_key do |f| %> +<% content_for :header_tags do %> + <%= stylesheet_link_tag "font-awesome.min.css", :plugin => "vault" %> + <%= javascript_include_tag 'vault', :plugin => 'vault' %> + <%= stylesheet_link_tag 'select2.min.css', :plugin => 'vault' %> + <%= javascript_include_tag 'select2.min', :plugin => 'vault' %> +<% end %> - -
- -
-
- -
-
-
<%= t('key.attr.type') %>
-
- <%= f.select :type, key_types, {}, { class: 'pwd-form-select', id: 'vault_key_type' } %> -
-
+

<%= t('key.title.edit') %>

-
-
<%= t('key.attr.name') %>
-
- <%= f.text_field :name, class: 'pwd-form-input', id: 'vault_key_name' %> -
-
- - <% if @key.created_at %> -
-
<%= t('key.attr.created_at') %>
-
- -
-
- <% end %> - - <% if @key.updated_at && @key.updated_at != @key.created_at %> -
-
<%= t('key.attr.updated_at') %>
-
- -
-
- <% end %> -
- - -
-
<%= t('key.attr.url') %>
-
- <%= f.text_field :url, class: 'pwd-form-input', id: 'vault_key_url' %> -
-
- - -
-
<%= t('key.attr.login') %>
-
- <%= f.text_field :login, class: 'pwd-form-input', id: 'vault_key_login' %> -
-
- - -
-
<%= t('key.attr.body') %>
-
- <%= f.text_field :body, autocomplete: :off, class: 'pwd-form-input', id: 'vault_key_body' %> - -
-
- - - <% if @key.type == 'Vault::KeyFile' || @key.type == 'Vault::Sftp' %> -
-
<%= t('key.attr.file') %>
-
- <% if @key.file.present? %> -
- Current file: - <%= @key.file %> -
- <% end %> - - <%= f.file_field :file, style: "display: none;", id: 'vault_key_file' %> -
-
- <% end %> - - -
-
<%= t('key.attr.tags') %>
-
- <%= f.text_field :tags, value: Vault::Tag.tags_to_string(@key.tags), class: 'pwd-form-input autocomplete', id: 'vault_key_tags' %> -
- <% if @key.persisted? %> - <%= link_to t('key.btn.edit_tags'), project_key_tags_path(@project, @key), class: 'pwd-edit-tags-link' %> - <% end %> -
- - -
-
<%= t('key.attr.comment') %>
-
- <%= f.text_area :comment, class: 'pwd-form-textarea wiki-edit', id: 'vault_key_comment' %> -
-
- - - <% if User.current.allowed_to?(:manage_whitelist_keys, @project) %> - <% whitelisted = @key.whitelist.split(",") %> - <% all_users = @project.memberships.active.where(:users => {:type => 'User'}).sort.map {|u| [u.principal, u.principal.id.to_s]} %> - <% all_groups = @project.memberships.active.where(:users => {:type => 'Group'}).sort.map {|u| [u.principal, u.principal.id.to_s]} %> - <% all_users = all_groups|all_users %> -
-
<%= t('key.attr.whitelist') %>
-
-
- <%= label_tag 'selected_users', t('key.whitelist.selected_members'), class: "pwd-whitelist-label" %> - <%= select_tag 'whitelist', - options_for_select(all_users.select {|u| whitelisted.include?(u[1])}), - id: 'selected_users', multiple: true, size: 8, class: 'pwd-whitelist-select' %> -
-
- - -
-
- <%= label_tag 'available_users', t('key.whitelist.available_members'), class: "pwd-whitelist-label" %> - <%= select_tag 'available_users', - options_for_select(all_users.select {|u| not whitelisted.include?(u[1])}), - id: 'available_users', multiple: true, size: 8, class: 'pwd-whitelist-select' %> -
-
-
- <% end %> -
-
- - -
- <%= render 'audit_log' %> -
-
- - - - - <% end %> -
- - - - +<%= render partial: 'form', locals: { key: @key, action: :update } %> diff --git a/app/views/keys/index.html.erb b/app/views/keys/index.html.erb index e6d49f1..666b0fe 100755 --- a/app/views/keys/index.html.erb +++ b/app/views/keys/index.html.erb @@ -1,4 +1,5 @@ -<% html_title(t('label_module')) %> +<% filtered_params = params.respond_to?(:to_unsafe_hash) ? params.to_unsafe_hash : params %> +<% html_title(@query.new_record? ? t('label_module') : @query.name) %> <% content_for :header_tags do %> <%= stylesheet_link_tag "font-awesome.min.css", :plugin => "vault" %> @@ -6,58 +7,83 @@ <% end %>
- <%= link_to t('key.btn.new'), new_project_key_path(@project), class: 'icon icon-add' if User.current.allowed_to?(:view_keys, @project) && User.current.allowed_to?(:edit_keys, @project) %> + <%= link_to_if_authorized sprite_icon('add', t('key.btn.new')), { :controller => 'keys', :action => 'new', :project_id => @project }, :class => 'icon icon-add' %>

- <%= t('key.title.list') %> + <%= @query.new_record? ? t('label_module') : h(@query.name) %> + + <%= text_field_tag(:search, @search, :autocomplete => 'off', :id => 'search', :class => 'live_search_field', :placeholder => t('key.btn.find'), :form => 'query_form') %> +

-<%= form_tag({}) do %> - - - - - - - - - - - - - - <% @keys.each do |key| %> - <%= render partial: key, locals: {parity: cycle('odd', 'even')} %> - <% end %> - -
- <%= check_box_tag 'check_all', '', false, :class => 'toggle-selection', - :title => "#{l(:button_check_all)}/#{l(:button_uncheck_all)}" %> - - <%= text_field_tag(:filter_type, '', - { id: 'filter-type', class: 'filter-text', placeholder: t('key.attr.type'), style: 'width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ddd;' }) %> - - <%= text_field_tag(:filter_name, '', - { id: 'filter-name', class: 'filter-text', placeholder: t('key.attr.name'), style: 'width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ddd;' }) %> - - <%= text_field_tag(:filter_url, '', - { id: 'filter-url', class: 'filter-text', placeholder: t('key.attr.url'), style: 'width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ddd;' }) %> - - <%= text_field_tag(:filter_login, '', - { id: 'filter-login', class: 'filter-text', placeholder: t('key.attr.login'), style: 'width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ddd;' }) %> - - <%= text_field_tag(:filter_body, '', - { id: 'filter-body', class: 'filter-text', placeholder: t('key.attr.body'), style: 'width: 100%; box-sizing: border-box; padding: 4px; border: 1px solid #ddd;' }) %> - <%= t('key.btn.actions') %>
+<%= form_tag({ :controller => 'keys', :action => 'index', :project_id => @project }, :method => :get, :id => 'query_form') do %> + + + <%= hidden_field_tag 'set_filter', '1' %> + +
+
+
+ + <%= l(:label_filter_plural) %> + +
+ <%= render :partial => 'queries/filters', :locals => { :query => @query } %> +
+
+ + +
+ +

+ <%= link_to_function sprite_icon('checked', l(:button_apply)), 'submit_query_form("query_form")', :class => 'icon icon-checked' %> + <%= link_to sprite_icon('reload', l(:button_clear)), { :set_filter => 1, :project_id => @project }, :class => 'icon icon-reload' %> +

+
<% end %> - - <%= pagination_links_full @key_pages, @key_count %> - +<%= error_messages_for 'query' %> +<% if @query.valid? %> +
+ <% if @keys.blank? %> +

<%= l(:label_no_data) %>

+ <% else %> + <%= render partial: 'list' %> + <%= pagination_links_full @key_pages, @key_count %> + <% end %> +
-<% other_formats_links do |f| %> - <%= f.link_to_with_query_parameters t('export.title.pdf') if User.current.allowed_to?(:view_keys, @project) && User.current.allowed_to?(:export_keys, @project)%> + <% other_formats_links do |f| %> + <%= f.link_to_with_query_parameters t('export.title.pdf') if User.current.allowed_to?(:view_keys, @project) && User.current.allowed_to?(:export_keys, @project) %> + <% end %> <% end %> <% content_for :sidebar do %> @@ -66,7 +92,7 @@ @@ -79,7 +105,6 @@ // Copy to clipboard functionality document.body.addEventListener('click', function(e) { - // Handle inline copy buttons (a.copy-key) var copyBtn = e.target.closest('a.copy-key'); if (copyBtn) { e.preventDefault(); @@ -97,50 +122,4 @@ } } }, true); - - // Client-side filtering for all columns (case-insensitive) - function applyFilters() { - var filters = { - 'filter-type': { selector: 'td:nth-child(2)', value: '' }, - 'filter-name': { selector: 'td:nth-child(3)', value: '' }, - 'filter-url': { selector: 'td:nth-child(4)', value: '' }, - 'filter-login': { selector: 'td:nth-child(5)', value: '' }, - 'filter-body': { selector: 'td:nth-child(6)', value: '' } - }; - - // Get all filter values - Object.keys(filters).forEach(function(filterId) { - var elem = document.getElementById(filterId); - if (elem) { - filters[filterId].value = elem.value.toLowerCase().trim(); - } - }); - - var rows = document.querySelectorAll('table.list tbody tr'); - rows.forEach(function(row) { - var matches = true; - - // Check each filter - Object.keys(filters).forEach(function(filterId) { - var filter = filters[filterId]; - if (filter.value) { - var cell = row.querySelector(filter.selector); - var text = cell ? cell.innerText.toLowerCase().trim() : ''; - if (!text.includes(filter.value)) { - matches = false; - } - } - }); - - row.style.display = matches ? '' : 'none'; - }); - } - - // Add event listeners to all filter fields - ['filter-type', 'filter-name', 'filter-url', 'filter-login', 'filter-body'].forEach(function(filterId) { - var elem = document.getElementById(filterId); - if (elem) { - elem.addEventListener('input', applyFilters); - } - }); diff --git a/app/views/keys/new.html.erb b/app/views/keys/new.html.erb index d5f4614..04772d1 100644 --- a/app/views/keys/new.html.erb +++ b/app/views/keys/new.html.erb @@ -1,669 +1,10 @@ -
- <%= form_for @key, url: { controller: :keys, action: :create }, as: :vault_key do |f| %> +<% content_for :header_tags do %> + <%= stylesheet_link_tag "font-awesome.min.css", :plugin => "vault" %> + <%= javascript_include_tag 'vault', :plugin => 'vault' %> + <%= stylesheet_link_tag 'select2.min.css', :plugin => 'vault' %> + <%= javascript_include_tag 'select2.min', :plugin => 'vault' %> +<% end %> - -
- -
-
- -
-
-
<%= t('key.attr.type') %>
-
- <%= f.select :type, key_types, {}, { class: 'pwd-form-select', id: 'vault_key_type' } %> -
-
+

<%= t('key.title.new') %>

-
-
<%= t('key.attr.name') %>
-
- <%= f.text_field :name, class: 'pwd-form-input', id: 'vault_key_name' %> -
-
-
- - -
-
<%= t('key.attr.url') %>
-
- <%= f.text_field :url, class: 'pwd-form-input', id: 'vault_key_url' %> -
-
- - -
-
<%= t('key.attr.login') %>
-
- <%= f.text_field :login, class: 'pwd-form-input', id: 'vault_key_login' %> -
-
- - -
-
<%= t('key.attr.body') %>
-
- <%= f.text_field :body, autocomplete: :off, class: 'pwd-form-input', id: 'vault_key_body' %> - -
-
- - - - - -
-
<%= t('key.attr.tags') %>
-
- <%= f.text_field :tags, value: Vault::Tag.tags_to_string(@key.tags), class: 'pwd-form-input autocomplete', id: 'vault_key_tags' %> -
-
- - -
-
<%= t('key.attr.comment') %>
-
- <%= f.text_area :comment, class: 'pwd-form-textarea wiki-edit', id: 'vault_key_comment' %> -
-
- - - <% if User.current.allowed_to?(:manage_whitelist_keys, @project) %> - <% whitelisted = @key.whitelist.split(",") %> - <% all_users = @project.memberships.active.where(:users => {:type => 'User'}).sort.map {|u| [u.principal, u.principal.id.to_s]} %> - <% all_groups = @project.memberships.active.where(:users => {:type => 'Group'}).sort.map {|u| [u.principal, u.principal.id.to_s]} %> - <% all_users = all_groups|all_users %> -
-
<%= t('key.attr.whitelist') %>
-
-
- <%= label_tag 'selected_users', t('key.whitelist.selected_members'), class: "pwd-whitelist-label" %> - <%= select_tag 'whitelist', - options_for_select(all_users.select {|u| whitelisted.include?(u[1])}), - id: 'selected_users', multiple: true, size: 8, class: 'pwd-whitelist-select' %> -
-
- - -
-
- <%= label_tag 'available_users', t('key.whitelist.available_members'), class: "pwd-whitelist-label" %> - <%= select_tag 'available_users', - options_for_select(all_users.select {|u| not whitelisted.include?(u[1])}), - id: 'available_users', multiple: true, size: 8, class: 'pwd-whitelist-select' %> -
-
-
- <% end %> -
-
-
- - - - - <% end %> -
- - - - \ No newline at end of file +<%= render partial: 'form', locals: { key: @key, action: :create } %> diff --git a/app/views/keys/show.html.erb b/app/views/keys/show.html.erb index e027e2a..8d495a5 100644 --- a/app/views/keys/show.html.erb +++ b/app/views/keys/show.html.erb @@ -1,538 +1,131 @@ <% content_for :header_tags do %> - <%= stylesheet_link_tag "font-awesome.css", :plugin => "vault" %> <%= stylesheet_link_tag "font-awesome.min.css", :plugin => "vault" %> <%= javascript_include_tag 'vault', :plugin => 'vault' %> <% end %> -
+<% html_title @key.name %> - -
- -
-
- -
-
-
<%= t('key.attr.type') %>
-
<%= @key.type %>
-
- -
-
<%= t('key.attr.name') %>
-
<%= @key.name %>
-
- -
-
<%= t('key.attr.created_at') %>
-
- -
-
- - <% if @key.updated_at && @key.updated_at != @key.created_at %> -
-
<%= t('key.attr.updated_at') %>
-
- -
-
- <% end %> -
- - - <% if @key.url.present? %> -
-
<%= t('key.attr.url') %>
- -
- <% end %> - - - <% if @key.login.present? %> -
-
<%= t('key.attr.login') %>
-
- <%= @key.login %> - -
-
- <% end %> - - - <% if @key.body.present? %> -
-
<%= t('key.attr.body') %>
-
- <%= @key.body %> - -
-
- <% end %> +
+ <%= link_to sprite_icon('edit', l(:button_edit)), edit_project_key_path(@project, @key), :class => 'icon icon-edit' if User.current.allowed_to?(:edit_keys, @project) %> + <%= link_to sprite_icon('del', l(:button_delete)), project_key_path(@project, @key), :data => { :confirm => t('confirm.key.delete') }, :method => :delete, :class => 'icon icon-del' if User.current.allowed_to?(:edit_keys, @project) %> +
- - <% if @key.type == 'Vault::KeyFile' || @key.type == 'Vault::Sftp' %> -
-
<%= t('key.attr.file') %>
- <% if @key.file.present? %> -
-
- <% file_path = File.join(Vault::KEYFILES_DIR, @key.file) %> - <%= link_to "#{@key.name}", "/projects/#{@project.identifier}/keys/#{@key.id}/download", class: 'pwd-file-link', title: "Download file" %> - <% if File.exist?(file_path) %> - <%= number_to_human_size(File.size(file_path)) %> - <% end %> -
-
- Local Path: - keys/<%= @key.file %> -
-
- <% else %> -
No file attached
- <% end %> -
- <% end %> +

<%= "#{t('key.title.show')} ##{@key.id}" %>

- +
+ + + + +
+

<%= h @key.name %>

+

+ <%= l(:label_updated_time, :value => time_tag(@key.updated_at || @key.created_at)).html_safe %> +

<% if @key.tags.any? %> -
-
<%= t('key.attr.tags') %>
-
- <% @key.tags.each do |tag| %> - - <%= tag.name %> - - <% end %> -
+
+ <% @key.tags.each do |tag| %> + <%= link_to tag.name, project_keys_path(@project, :search => "##{tag.name}"), :class => 'tag-label-color', :style => "background-color: #{tag.color}; color: #fff; border-radius: 3px; padding: 2px 6px;" %> + <% end %>
<% end %> - - - <% if @key.comment.present? %> -
-
<%= t('key.attr.comment') %>
-
- <%= simple_format(@key.comment) %> -
-
+
+ +
+

+ + <%= t('key.attr.type') %>: <%= @key.type.demodulize %> +

+ + <% if @key.url.present? %> +

+ + <%= link_to @key.url, @key.url, :target => '_blank' %> + + + + +

+ <% end %> + + <% if @key.login.present? %> +

+ + <%= h @key.login %> + + + + +

+ <% end %> + + <% if @key.body.present? %> +

+ + •••••••• + + + + +

+ <% end %> + + <% if (@key.type == 'Vault::KeyFile' || @key.type == 'Vault::Sftp') && @key.file.present? %> +

+ + <%= link_to @key.name, "/projects/#{@project.identifier}/keys/#{@key.id}/download" %> + <% file_path = File.join(Vault::KEYFILES_DIR, @key.file) %> + <% if File.exist?(file_path) %> + (<%= number_to_human_size(File.size(file_path)) %>) <% end %> - -

-
- - -
- <%= render 'audit_log' %> -
+

+ <% end %> + + <% if @key.created_at %> +

+ + <%= format_time(@key.created_at) %> +

+ <% end %> + + <% if @key.updated_at && @key.updated_at != @key.created_at %> +

+ + <%= format_time(@key.updated_at) %> +

+ <% end %>
- - + <% if @key.comment.present? %> +
+

<%= t('key.attr.comment') %>

+
<%= simple_format(h(@key.comment)) %>
+ <% end %>
- + }, true); + diff --git a/app/views/shared/_key_actions.html.erb b/app/views/shared/_key_actions.html.erb index 2a941cb..49ad760 100644 --- a/app/views/shared/_key_actions.html.erb +++ b/app/views/shared/_key_actions.html.erb @@ -14,18 +14,15 @@ <% end %> <% elsif User.current.allowed_to?(:edit_keys, key.project) %> - <%= link_to project_key_path(key.project,key), class: 'keys-actions', title: t('button_view') do %> - + + <%= link_to project_copy_key_path(key.project,key), class: 'keys-actions', title: t('key.title.copy') do %> + <% end %> <%= link_to edit_project_key_path(key.project,key), class: 'keys-actions', title: t('button_edit') do %> <% end %> - <%= link_to project_copy_key_path(key.project,key), class: 'keys-actions', title: t('key.title.copy') do %> - - <% end %> - <%= link_to project_key_path(key.project,key), class: 'keys-actions', title: t('button_delete'), data: {confirm: t('confirm.key.delete')}, method: :delete do %> <% end %> diff --git a/app/views/shared/_key_fields.html.erb b/app/views/shared/_key_fields.html.erb index 1280897..584c066 100644 --- a/app/views/shared/_key_fields.html.erb +++ b/app/views/shared/_key_fields.html.erb @@ -1,68 +1,66 @@ - - - - - <% if key.project.nil? %> - - <%= key.name %> - - <% else %> - <%= link_to key.name, edit_project_key_path(key.project, key), class: 'keys-links' %> - <% end %> - - - <% unless key.url.blank? %> - - '> - - - <% if /:\/\//.match(key.url) %> - <%= link_to "#{key.url}", key.url, class: 'keys-links'%> +<% query = local_assigns.fetch(:query, @query) %> +<% query.inline_columns.each do |column| %> + + <% case column.name.to_s %> + <% when 'type' %> + <% type_label = case key.type + when 'Vault::Password' then t('activerecord.models.password') + when 'Vault::Sftp' then t('activerecord.models.sftp') + when 'Vault::KeyFile' then t('activerecord.models.key_file') + else key.type.demodulize + end %> + <% if key.project.nil? %> + <%= type_label %> + <% else %> + + <% end %> + <% when 'name' %> + <% if key.project.nil? %> + <%= key.name %> <% else %> - + <%= link_to key.name, project_key_path(key.project, key), class: 'keys-links' %> + <% end %> + <% when 'url' %> + <% unless key.url.blank? %> + + '> + + + <% if /:\/\//.match(key.url) %> + <%= link_to key.url, key.url, class: 'keys-links' %> + <% else %> + + <% end %> + + <% end %> - - - <% end %> - - - <% unless key.login.blank? %> - + <% end %> + <% when 'body' %> + <% if (key.type == 'Vault::Sftp' || key.type == 'Vault::KeyFile') && key.file.present? && key.project.present? %> + <%= link_to "/projects/#{key.project.identifier}/keys/#{key.id}/download", class: 'keys-actions', title: 'Download file', id: "keyfile_dl_#{key.id}" do %> + + <% end %> + <% end %> + <% unless key.body.blank? %> + '> + - - - - <% end %> - + •••••••• + + <% end %> + <% else %> + <%= column_content(column, key) %> + <% end %> + +<% end %> diff --git a/app/views/vault/key_files/_key_file.html.erb b/app/views/vault/key_files/_key_file.html.erb index 797e668..8b79110 100644 --- a/app/views/vault/key_files/_key_file.html.erb +++ b/app/views/vault/key_files/_key_file.html.erb @@ -1,22 +1,9 @@ -'> - +' style='cursor:pointer;' onclick="window.location='<%= key_file.project ? project_key_path(key_file.project, key_file) : '#' %>'"> + <%= check_box_tag('ids[]', key_file.id, false, id: nil) %> <%= render partial: 'shared/key_fields', locals: {key: key_file} %> - - <% unless key_file.file.blank? %> - <%= link_to "/projects/#{@project.identifier}/keys/#{key_file.id}/download", class: 'keys-actions', title: "Download file", id: "keyfile_dl_#{key_file.id}" do %> - - <% end %> - <% end %> - <% unless key_file.body.blank? %> - '> - - - - <% end %> - - + <%= render partial: 'shared/key_actions', locals: {key: key_file} %> diff --git a/app/views/vault/passwords/_password.html.erb b/app/views/vault/passwords/_password.html.erb index 8ab0719..a5b5bfa 100644 --- a/app/views/vault/passwords/_password.html.erb +++ b/app/views/vault/passwords/_password.html.erb @@ -1,17 +1,9 @@ -'> - +' style='cursor:pointer;' onclick="window.location='<%= password.project ? project_key_path(password.project, password) : '#' %>'"> + <%= check_box_tag('ids[]', password.id, false, id: nil) %> <%= render partial: 'shared/key_fields', locals: {key: password} %> - - <% unless password.body.blank? %> - '> - - - - <% end %> - - + <%= render partial: 'shared/key_actions', locals: {key: password} %> diff --git a/app/views/vault/sftps/_sftp.html.erb b/app/views/vault/sftps/_sftp.html.erb index 881e1b6..412f102 100644 --- a/app/views/vault/sftps/_sftp.html.erb +++ b/app/views/vault/sftps/_sftp.html.erb @@ -1,22 +1,9 @@ -'> - +' style='cursor:pointer;' onclick="window.location='<%= sftp.project ? project_key_path(sftp.project, sftp) : '#' %>'"> + <%= check_box_tag('ids[]', sftp.id, false, id: nil) %> <%= render partial: 'shared/key_fields', locals: {key: sftp} %> - - <% if sftp.file.present? %> - <%= link_to "/projects/#{@project.identifier}/keys/#{sftp.id}/download", class: 'keys-actions', title: "Download file" do %> - - <% end %> - <% end %> - <% unless sftp.body.blank? %> - '> - - - - <% end %> - - + <%= render partial: 'shared/key_actions', locals: {key: sftp} %> diff --git a/assets/javascripts/select2.min.js b/assets/javascripts/select2.min.js new file mode 100644 index 0000000..9def5ae --- /dev/null +++ b/assets/javascripts/select2.min.js @@ -0,0 +1,2 @@ +/*! Select2 4.0.8 | https://github.com/select2/select2/blob/master/LICENSE.md */ +!function(n){"function"==typeof define&&define.amd?define(["jquery"],n):"object"==typeof module&&module.exports?module.exports=function(e,t){return void 0===t&&(t="undefined"!=typeof window?require("jquery"):require("jquery")(e)),n(t),t}:n(jQuery)}(function(u){var e=function(){if(u&&u.fn&&u.fn.select2&&u.fn.select2.amd)var e=u.fn.select2.amd;var t,n,r,h,o,s,f,g,m,v,y,_,i,a,w;function b(e,t){return i.call(e,t)}function l(e,t){var n,r,i,o,s,a,l,c,u,d,p,h=t&&t.split("/"),f=y.map,g=f&&f["*"]||{};if(e){for(s=(e=e.split("/")).length-1,y.nodeIdCompat&&w.test(e[s])&&(e[s]=e[s].replace(w,"")),"."===e[0].charAt(0)&&h&&(e=h.slice(0,h.length-1).concat(e)),u=0;u":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t]},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('
    ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h('
  • '),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):ithis.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=(e.id,e.id+"-results");this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex)}),e.on("disable",function(){n.$selection.attr("tabindex","-1")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){n(this);this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r),n.attr("title",t.title||t.text)}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,a){function n(e,t){n.__super__.constructor.apply(this,arguments)}return a.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
      '),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.options.get("disabled")){var t=i(this).parent(),n=a.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('
    • ×
    • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,s,a){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){r.$search.attr("aria-activedescendant",e.id)}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===a.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.query=function(n,r,i){var o=this;this.current(function(e){var t=null!=e?e.length:0;0=o.maximumSelectionLength?o.trigger("results:message",{message:"maximumSelected",args:{maximum:o.maximumSelectionLength}}):n.call(o,r,i)})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(i,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=i('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){i(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
    • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=n.get("dropdownParent")||f(document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this,i=!1;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),i||(i=!0,t.on("results:all",function(){r._positionDropdown(),r._resizeDropdown()}),t.on("results:append",function(){r._positionDropdown(),r._resizeDropdown()}))}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h=p.offset();d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/assets/stylesheets/vault.css b/assets/stylesheets/vault.css index 111bc6a..f27f231 100644 --- a/assets/stylesheets/vault.css +++ b/assets/stylesheets/vault.css @@ -52,6 +52,9 @@ a.keys-links:hover, a.keys-links:active { color: #c61a1a; text-decoration: none; display: flex; align-items: center; } +.controller-keys #content .keys-search-title .live_search { + vertical-align: middle; +} .controller-keys #content .keys-search-form { padding-left: 15px; } @@ -84,4 +87,21 @@ a.keys-links:hover, a.keys-links:active { color: #c61a1a; text-decoration: none; .pwd-form-select { color: transparent !important; background-color: #fff !important; -} \ No newline at end of file +} + +/* Tag box widget */ +.vault-tag-type-input-field { + /* inherits .tabular input styles naturally */ +} +.vault-tag-chips { + display: flex; + flex-wrap: wrap; + gap: 4px; + margin-left: 180px; + padding: 4px 0 6px 0; + min-height: 0; +} +.vault-tag-chips:empty { + display: none; +} +/* Select2 tag color chip overrides */ diff --git a/config/locales/de.yml b/config/locales/de.yml index 6910d9f..9d95a19 100644 --- a/config/locales/de.yml +++ b/config/locales/de.yml @@ -1,9 +1,13 @@ de: + button_view: "Anzeigen" label_vault: 'Vault' label_module: 'Passwörter' + field_has_url: 'Hat URL' + field_has_login: 'Hat Login' + field_body: 'Passwort' settings: title: "Vault Einstellungen" - vault_title_encription_key: "Encryption key" + vault_title_encription_key: "Verschlüsselungsschlüssel" redmine_title_encription_key: "Verschlüsselung von Redmine verwenden" vault_encription_notice: "Vault-eigene Verschlüsselung verwenden" redmine_encryption_notice: "Empfohlen. Der Schlüssel muss unter 'database_cipher_key' in der Konfiguration config/configuration.yml hinterlegt werden. WICHTIG: führe danach »rake redmine:plugins:vault:convert« aus." @@ -12,9 +16,10 @@ de: models: password: "Passwört" sftp: "SFTP" - + key_file: "Passwortdatei" + backups: - title: "Passwortbackup " + title: "Passwortbackup" title_restore: "aus Backup wiederherstellen" btn: download_backup: "Backup erstellen" @@ -28,7 +33,7 @@ de: tag: title: - popular: "beliebte Tags" + popular: "Beliebte Tags" key: title: @@ -37,14 +42,20 @@ de: copy: "Passwort kopieren" clone: "Passwort duplizieren" show: "Passwort anzeigen" - new: "neues Passwort" + new: "Neues Passwort" list: "Passwörter" + project_not_exist: "Projekt mit ID %{id} existiert nicht" + project_id: "ID:" + btn: - new: "neues Passwort" + new: "Neues Passwort" actions: "Aktionen" clipboard: "In die Zwischenablage kopieren" + generate: "Generieren" find: "Suchen" + move: "Projekt zuweisen" + edit_tags: "Tags bearbeiten" attr: lock: "Lock" @@ -54,14 +65,26 @@ de: login: "Benutzername" url: "URL" type: "Typ" + project: "Projekt" tags: "Tags" file: "Schlüsseldatei" comment: "Kommentar" whitelist: "Zugriffsliste" + created_at: "Erstellt" + updated_at: "Geändert" whitelist: selected_members: "Benutzer mit Zugriff" - available_members: "verfügbare Benutzer" + available_members: "Verfügbare Benutzer" + + audit_log: + title: "Audit-Log" + action: "Aktion" + user: "Benutzer" + fields: "Geänderte Felder" + timestamp: "Zeitstempel" + no_entries: "Keine Einträge" + ago: "vor" context_menu: copy_key: "Passwort kopieren" @@ -95,5 +118,17 @@ de: key: not_set: "Encryption key wurde nicht in den Einstellungen hinterlegt" not_whitelisted: "Kein Zugriff auf das Passwort" + length: "Der Verschlüsselungsschlüssel muss genau 16 Zeichen lang sein" + not_orphaned: "Dieses Passwort ist nicht verwaist" + project: + required: "Projekt ist erforderlich" user: not_allowed: "Sie sind nicht berechtigt, diese Passwörter anzusehen" + + permission_export_keys: "Passwörter exportieren" + permission_keys_all: "Alle Passwörter ansehen" + permission_download_keys: "Passwortdateien herunterladen" + permission_view_keys: "Passwörter ansehen" + permission_edit_keys: "Passwörter bearbeiten" + permission_manage_whitelist_keys: "Zugriffsliste verwalten" + permission_whitelist_keys: "Zugriffsliste" diff --git a/config/locales/en.yml b/config/locales/en.yml index 177d3ba..9141671 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -3,6 +3,9 @@ en: button_view: 'View' label_vault: 'Vault' label_module: 'Passwords' + field_has_url: 'Has URL' + field_has_login: 'Has login' + field_body: 'Password' settings: title: "Passwords settings" vault_title_encription_key: "Encryption Key" @@ -14,6 +17,7 @@ en: models: password: "Password" sftp: "SFTP" + key_file: "Password File" backups: title: "Backup Passwords" @@ -49,6 +53,7 @@ en: new: "New Password" actions: "Actions" clipboard: "Copy to Clipboard" + generate: "Generate" find: "Find" move: "Assign to Project" edit_tags: "Edit Tags" @@ -120,3 +125,11 @@ en: required: "Project is required" user: not_allowed: "You are not allowed to view this passwords" + + permission_export_keys: "Export passwords" + permission_keys_all: "View all passwords" + permission_download_keys: "Download password files" + permission_view_keys: "View passwords" + permission_edit_keys: "Edit passwords" + permission_manage_whitelist_keys: "Manage password access list" + permission_whitelist_keys: "Access list" diff --git a/config/locales/es.yml b/config/locales/es.yml index 0fcacf2..4932893 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -1,7 +1,11 @@ # Spanish strings go here for Rails i18n es: + button_view: "Ver" label_vault: 'Vault' label_module: 'Claves' + field_has_url: 'Tiene URL' + field_has_login: 'Tiene usuario' + field_body: 'Clave' settings: title: "Configuración de Vault" vault_title_encription_key: "Clave de encriptación" @@ -13,6 +17,7 @@ es: models: password: "Clave" sftp: "SFTP" + key_file: "Archivo de clave" backups: title: "Copia de seguridad de las claves" @@ -41,11 +46,17 @@ es: new: "Nueva Clave" list: "Claves" + project_not_exist: "El proyecto con ID %{id} no existe" + project_id: "ID:" + btn: new: "Nueva Clave" actions: "Acciones" clipboard: "Copiar al portapapeles" + generate: "Generar" find: "Buscar" + move: "Asignar al Proyecto" + edit_tags: "Editar Etiquetas" attr: lock: "Bloquear" @@ -55,15 +66,27 @@ es: login: "Usuario" url: "URL" type: "Tipo" + project: "Proyecto" tags: "Etiquetas" file: "Archivo de Clave" comment: "Comentario" whitelist: "Lista de accesos" + created_at: "Creado" + updated_at: "Actualizado" whitelist: selected_members: "Miembros seleccionados" available_members: "Miembros disponibles" + audit_log: + title: "Registro de auditoría" + action: "Acción" + user: "Usuario" + fields: "Campos modificados" + timestamp: "Marca de tiempo" + no_entries: "Sin entradas de auditoría" + ago: "hace" + context_menu: copy_key: "Copiar Clave" copy_url: "Copiar URL" @@ -96,5 +119,17 @@ es: key: not_set: "Clave de encriptación no está configurada" not_whitelisted: "Usted no tiene acceso a esta clave" + length: "La clave de encriptación debe tener exactamente 16 símbolos" + not_orphaned: "Esta clave no es huérfana" + project: + required: "El proyecto es obligatorio" user: - not_allowed: "No tienes permiso para ver estas contraseñas" \ No newline at end of file + not_allowed: "No tienes permiso para ver estas contraseñas" + + permission_export_keys: "Exportar contraseñas" + permission_keys_all: "Ver todas las contraseñas" + permission_download_keys: "Descargar archivos de contraseña" + permission_view_keys: "Ver contraseñas" + permission_edit_keys: "Editar contraseñas" + permission_manage_whitelist_keys: "Gestionar lista de acceso" + permission_whitelist_keys: "Lista de acceso" diff --git a/config/locales/fr.yml b/config/locales/fr.yml index 668f7b4..2191af7 100644 --- a/config/locales/fr.yml +++ b/config/locales/fr.yml @@ -1,19 +1,24 @@ -# French strings go here for Rails i18n (Translate from en and it version) +# French strings go here for Rails i18n fr: + button_view: "Voir" label_vault: 'Vault' - label_module: 'clefs' + label_module: 'Clefs' + field_has_url: 'A une URL' + field_has_login: 'A un login' + field_body: 'Clef' settings: - title: "Vault settings" - vault_title_encription_key: "clef de chiffrement" + title: "Paramètres Vault" + vault_title_encription_key: "Clef de chiffrement" redmine_title_encription_key: "Utiliser le chiffrement de Redmine" vault_encription_notice: "Utilisé uniquement avec la méthode de chiffrement de Vault" - redmine_encryption_notice: "Recommandé. N'oubliez pas de mettre votre clef dans l'option database_cipher_key du fichier config/configuration.yml. IMPORTANT: Lancez redmine:plugins:vault:convert aprèrs tout changement de ce paramètre." + redmine_encryption_notice: "Recommandé. N'oubliez pas de mettre votre clef dans l'option database_cipher_key du fichier config/configuration.yml. IMPORTANT: Lancez redmine:plugins:vault:convert après tout changement de ce paramètre." activerecord: models: password: "Clef" sftp: "SFTP" - + key_file: "Fichier de clé" + backups: title: "Sauvegarder les clefs" title_restore: "Restaurer depuis une sauvegarde" @@ -41,11 +46,17 @@ fr: new: "Nouvelle clef" list: "Clefs" + project_not_exist: "Le projet avec l'ID %{id} n'existe pas" + project_id: "ID :" + btn: new: "Nouvelle clef" actions: "Actions" - clipboard: "Copier vers le presse papier" - find: "Recherche" + clipboard: "Copier vers le presse-papier" + generate: "Générer" + find: "Rechercher" + move: "Assigner au projet" + edit_tags: "Modifier les tags" attr: lock: "Verrou" @@ -55,15 +66,27 @@ fr: login: "Login" url: "URL" type: "Type" + project: "Projet" tags: "Tags" - file: "Fichier de le clef" + file: "Fichier de clef" comment: "Commentaire" whitelist: "Autorisation des membres" + created_at: "Créé" + updated_at: "Modifié" whitelist: selected_members: "Membres autorisés" available_members: "Membres disponibles" + audit_log: + title: "Journal d'audit" + action: "Action" + user: "Utilisateur" + fields: "Champs modifiés" + timestamp: "Horodatage" + no_entries: "Aucune entrée d'audit" + ago: "il y a" + context_menu: copy_key: "Copier la clef" copy_url: "Copier l'URL" @@ -78,12 +101,12 @@ fr: confirm: key: - delete: "Êtes vous sur de vouloir supprimer cette clef ?" + delete: "Êtes-vous sûr de vouloir supprimer cette clef ?" notice: settings: saved: "Paramètres sauvegardés" - keys_restore: "Clefs restaurés" + keys_restore: "Clefs restaurées" key: update: success: "La clef a été mise à jour" @@ -95,6 +118,18 @@ fr: error: key: not_set: "La clé de chiffrement n'est pas déclarée dans la configuration" - not_whitelisted: "Clef non autoirisée à la consultation" + not_whitelisted: "Clef non autorisée à la consultation" + length: "La clef de chiffrement doit faire exactement 16 caractères" + not_orphaned: "Cette clef n'est pas orpheline" + project: + required: "Le projet est requis" user: - not_allowed: "Vous n'êtes pas autorisé à voir ces mots de passe" \ No newline at end of file + not_allowed: "Vous n'êtes pas autorisé à voir ces mots de passe" + + permission_export_keys: "Exporter les mots de passe" + permission_keys_all: "Voir tous les mots de passe" + permission_download_keys: "Télécharger les fichiers de mot de passe" + permission_view_keys: "Voir les mots de passe" + permission_edit_keys: "Modifier les mots de passe" + permission_manage_whitelist_keys: "Gérer la liste d'accès" + permission_whitelist_keys: "Liste d'accès" diff --git a/config/locales/it.yml b/config/locales/it.yml index 94bb57b..db1f4c6 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -1,7 +1,11 @@ # Italian strings go here for Rails i18n it: + button_view: "Visualizza" label_vault: 'Vault' - label_module: 'Keys' + label_module: 'Chiavi' + field_has_url: 'Ha URL' + field_has_login: 'Ha login' + field_body: 'Password' settings: title: "Impostazioni Vault" vault_title_encription_key: "Chiave crittografica" @@ -13,19 +17,20 @@ it: models: password: "Chiavi" sftp: "SFTP" + key_file: "File chiave" backups: title: "Backup chiavi" title_restore: "Ripristinare dal backup" btn: download_backup: "Download backup" - upload_backup: "Restore from backup" + upload_backup: "Ripristina dal backup" export: title: pdf: "PDF" pdf: - title: "chiavi" + title: "Chiavi" tag: title: @@ -39,13 +44,19 @@ it: clone: "Clona Chiave" show: "Mostra la chiave" new: "Nuova Chiave" - list: "Chiave" + list: "Chiavi" + + project_not_exist: "Il progetto con ID %{id} non esiste" + project_id: "ID:" btn: new: "Nuova Chiave" actions: "Azioni" clipboard: "Copia negli appunti" + generate: "Genera" find: "Ricerca" + move: "Assegna al Progetto" + edit_tags: "Modifica Tag" attr: lock: "Blocca" @@ -55,15 +66,27 @@ it: login: "Utente" url: "URL" type: "Tipo" + project: "Progetto" tags: "Tag" file: "File Chiave" comment: "Commento" whitelist: "Elenco di accesso" + created_at: "Creato" + updated_at: "Aggiornato" whitelist: selected_members: "Membri selezionati" available_members: "Membri disponibili" + audit_log: + title: "Log di audit" + action: "Azione" + user: "Utente" + fields: "Campi modificati" + timestamp: "Data/Ora" + no_entries: "Nessuna voce di audit" + ago: "fa" + context_menu: copy_key: "Copia Chiave" copy_url: "Copia URL" @@ -96,5 +119,17 @@ it: key: not_set: "La chiave crittografata non è definita nelle impostazioni" not_whitelisted: "Chiave non consentita per te" + length: "La chiave crittografica deve essere di esattamente 16 caratteri" + not_orphaned: "Questa chiave non è orfana" + project: + required: "Il progetto è obbligatorio" user: - not_allowed: "Non sei autorizzato a visualizzare queste password" \ No newline at end of file + not_allowed: "Non sei autorizzato a visualizzare queste password" + + permission_export_keys: "Esporta password" + permission_keys_all: "Visualizza tutte le password" + permission_download_keys: "Scarica file password" + permission_view_keys: "Visualizza password" + permission_edit_keys: "Modifica password" + permission_manage_whitelist_keys: "Gestisci lista di accesso" + permission_whitelist_keys: "Lista di accesso" diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 290b4e1..8288eae 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1,100 +1,135 @@ # Japanese strings go here for Rails i18n ja: + button_view: "表示" label_vault: 'Vault' - label_module: 'Keys' + label_module: 'パスワード' + field_has_url: 'URLあり' + field_has_login: 'ログインあり' + field_body: 'パスワード' settings: title: "Vault 設定" vault_title_encription_key: "暗号化キー" - redmine_title_encription_key: "Use Redmine encryption" - vault_encription_notice: "Used only by Vault encryption method" - redmine_encryption_notice: "Recommended. Don't forget to put your key into database_cipher_key in config/configuration.yml. IMPORTANT: run rake redmine:plugins:vault:convert after changing this switch." + redmine_title_encription_key: "Redmine暗号化を使用" + vault_encription_notice: "Vault暗号化メソッドのみで使用" + redmine_encryption_notice: "推奨。config/configuration.ymlのdatabase_cipher_keyにキーを設定してください。重要: この設定変更後、rake redmine:plugins:vault:convertを実行してください。" activerecord: models: - password: "Password" + password: "パスワード" sftp: "SFTP" + key_file: "パスワードファイル" - backups: - title: "Backup keys" - title_restore: "Restore from backup" - btn: - download_backup: "Download backup" - upload_backup: "Restore from backup" + backups: + title: "パスワードのバックアップ" + title_restore: "バックアップから復元" + btn: + download_backup: "バックアップをダウンロード" + upload_backup: "バックアップから復元" - export: - title: - pdf: "PDF" - pdf: - title: "keys" + export: + title: + pdf: "PDF" + pdf: + title: "パスワード" - tag: - title: - popular: "Popular Tags" + tag: + title: + popular: "人気のタグ" + key: + title: + delete: "パスワードを削除" + edit: "パスワードを編集" + copy: "パスワードをコピー" + clone: "パスワードを複製" + show: "パスワードを表示" + new: "新しいパスワード" + list: "パスワード一覧" + + project_not_exist: "ID %{id} のプロジェクトが存在しません" + project_id: "ID:" + + btn: + new: "新しいパスワード" + actions: "操作" + clipboard: "クリップボードにコピー" + generate: "生成" + find: "検索" + move: "プロジェクトに割り当て" + edit_tags: "タグを編集" + + attr: + lock: "ロック" + owner: "作成者" + body: "パスワード" + name: "名前" + login: "ログイン" + url: "URL" + type: "種類" + project: "プロジェクト" + tags: "タグ" + file: "パスワードファイル" + comment: "コメント" + whitelist: "アクセスリスト" + created_at: "作成日時" + updated_at: "更新日時" + + whitelist: + selected_members: "選択されたメンバー" + available_members: "利用可能なメンバー" + + audit_log: + title: "監査ログ" + action: "操作" + user: "ユーザー" + fields: "変更されたフィールド" + timestamp: "タイムスタンプ" + no_entries: "監査項目なし" + ago: "前" + + context_menu: + copy_key: "パスワードをコピー" + copy_url: "URLをコピー" + copy_login: "ログインをコピー" + delete: "パスワードを削除" + edit: "パスワードを編集" + clone: "パスワードを複製" + + alert: key: - title: - delete: "Delete Key" - edit: "Editing Key" - copy: "Copy Key" - clone: "Clone Key" - show: "Showing Key" - new: "New Key" - list: "Listing Keys" - - btn: - new: "New Key" - actions: "Actions" - clipboard: "Copy to clipboard" - find: "Find" - - attr: - lock: "Lock" - owner: "Owner" - body: "Key" - name: "Name" - login: "Login" - url: "URL" - type: "Type" - tags: "Tags" - file: "Key File" - comment: "Comment" - whitelist: "Access list" - - whitelist: - selected_members: "Selected Members" - available_members: "Available Members" - - context_menu: - copy_key: "Copy Key" - copy_url: "Copy URL" - copy_login: "Copy Login" - delete: "Delete Key" - edit: "Edit Key" - clone: "Clone Key" - - alert: - key: - not_found: "Key not found" - - confirm: - key: - delete: "Do you want to delete this key?" - - notice: - settings: - saved: "Settings saved" - keys_restore: "Keys restored" - key: - update: - success: "Key was successfully modified" - delete: - success: "Key was successfully deleted" - create: - success: "Key was successfully created" - - error: - key: - not_set: "The encryption key is not set in the settings" - not_whitelisted: "Key not allowed for you" - user: - not_allowed: "You are not allowed to view this passwords" \ No newline at end of file + not_found: "パスワードが見つかりません" + + confirm: + key: + delete: "このパスワードを削除してもよろしいですか?" + + notice: + settings: + saved: "設定を保存しました" + keys_restore: "パスワードを復元しました" + key: + update: + success: "パスワードを更新しました" + delete: + success: "パスワードを削除しました" + create: + success: "パスワードを作成しました" + + error: + key: + not_set: "設定で暗号化キーが設定されていません" + not_whitelisted: "このパスワードへのアクセスが許可されていません" + length: "暗号化キーは正確に16文字でなければなりません" + not_orphaned: "このパスワードは孤立していません" + project: + required: "プロジェクトは必須です" + user: + not_allowed: "これらのパスワードを表示する権限がありません" + + permission_export_keys: "パスワードをエクスポート" + permission_keys_all: "全パスワードを表示" + permission_download_keys: "パスワードファイルをダウンロード" + permission_view_keys: "パスワードを表示" + permission_edit_keys: "パスワードを編集" + permission_manage_whitelist_keys: "アクセスリストを管理" + permission_whitelist_keys: "アクセスリスト" diff --git a/config/locales/nl.yml b/config/locales/nl.yml index 8c1da77..0a4b1b8 100644 --- a/config/locales/nl.yml +++ b/config/locales/nl.yml @@ -1,18 +1,23 @@ # Dutch strings go here for Rails i18n nl: + button_view: "Bekijken" label_vault: 'Kluis' - label_module: 'Keys' + label_module: 'Sleutels' + field_has_url: 'Heeft URL' + field_has_login: 'Heeft login' + field_body: 'Wachtwoord' settings: title: "Kluis instellingen" vault_title_encription_key: "Encryptiesleutel" redmine_title_encription_key: "Gebruik Redmine encryptie" vault_encription_notice: "Wordt alleen gebruikt door de encryptie methode van de kluis" - redmine_encryption_notice: "Aanbevolen. Vergeet niet om jouw eigen sluitel in de database_cipher_key in config/configuration toe te passen. Belangrijk: voer het commando: \"rake redmine:plugins:vault:convert\" nadat je deze wijzigingen hebt toegepast." + redmine_encryption_notice: "Aanbevolen. Vergeet niet om jouw eigen sleutel in de database_cipher_key in config/configuration.yml toe te passen. Belangrijk: voer het commando \"rake redmine:plugins:vault:convert\" nadat je deze wijzigingen hebt toegepast." activerecord: models: - password: "Password" + password: "Wachtwoord" sftp: "SFTP" + key_file: "Wachtwoordbestand" backups: title: "Backup sleutels" @@ -25,7 +30,7 @@ nl: title: pdf: "PDF" pdf: - title: "sleutels" + title: "Sleutels" tag: title: @@ -39,13 +44,19 @@ nl: clone: "Dupliceer sleutel" show: "Resultaat sleutel" new: "Nieuwe sleutel" - list: "Listing Keys" + list: "Sleutels" + + project_not_exist: "Project met ID %{id} bestaat niet" + project_id: "ID:" btn: new: "Nieuwe sleutel" actions: "Acties" clipboard: "Kopieer naar klembord" + generate: "Genereren" find: "Zoekopdracht" + move: "Aan project toewijzen" + edit_tags: "Tags bewerken" attr: lock: "Sluit" @@ -55,15 +66,27 @@ nl: login: "Login" url: "URL" type: "Type" + project: "Project" tags: "Tags" file: "Sleutel bestand" comment: "Commentaar" whitelist: "Toegangslijst" + created_at: "Aangemaakt" + updated_at: "Bijgewerkt" whitelist: selected_members: "Geselecteerde leden" available_members: "Beschikbare leden" + audit_log: + title: "Auditlog" + action: "Actie" + user: "Gebruiker" + fields: "Gewijzigde velden" + timestamp: "Tijdstempel" + no_entries: "Geen auditrecords" + ago: "geleden" + context_menu: copy_key: "Kopieer sleutel" copy_url: "Kopieer URL" @@ -82,8 +105,8 @@ nl: notice: settings: - saved: "Settings saved" - keys_restore: "Keys restored" + saved: "Instellingen opgeslagen" + keys_restore: "Sleutels hersteld" key: update: success: "Sleutel succesvol aangepast" @@ -96,5 +119,17 @@ nl: key: not_set: "De encryptie sleutel is niet gezet in de instellingen" not_whitelisted: "Sleutel niet toegestaan voor jou" + length: "De encryptiesleutel moet exact 16 tekens lang zijn" + not_orphaned: "Deze sleutel is niet weesloos" + project: + required: "Project is vereist" user: - not_allowed: "U mag deze wachtwoorden niet bekijken" \ No newline at end of file + not_allowed: "U mag deze wachtwoorden niet bekijken" + + permission_export_keys: "Wachtwoorden exporteren" + permission_keys_all: "Alle wachtwoorden bekijken" + permission_download_keys: "Wachtwoordbestanden downloaden" + permission_view_keys: "Wachtwoorden bekijken" + permission_edit_keys: "Wachtwoorden bewerken" + permission_manage_whitelist_keys: "Toegangslijst beheren" + permission_whitelist_keys: "Toegangslijst" diff --git a/config/locales/pt-BR.yml b/config/locales/pt-BR.yml index e767662..6dcb628 100644 --- a/config/locales/pt-BR.yml +++ b/config/locales/pt-BR.yml @@ -1,17 +1,24 @@ pt-BR: + project_module_keys: Acessos + + button_view: 'Visualizar' label_vault: 'Senhas' label_module: 'Acessos' + field_has_url: 'Tem URL' + field_has_login: 'Tem login' + field_body: 'Senha' settings: title: "Configuração de senhas" vault_title_encription_key: "Criptografia da chave" redmine_title_encription_key: "Use Criptografia Redmine" vault_encription_notice: "Usado apenas pelo método de criptografia do Vault" - redmine_encryption_notice: "Recomendado. Não esqueça de colocar sua chave em database_cipher_key em config / configuration.yml. IMPORTANTE: execute o rake redmine: plugins: vault: converte depois de mudar este interruptor." + redmine_encryption_notice: "Recomendado. Não esqueça de colocar sua chave em database_cipher_key em config/configuration.yml. IMPORTANTE: execute o rake redmine:plugins:vault:convert depois de mudar este interruptor." activerecord: models: password: "Senha" sftp: "SFTP" + key_file: "Arquivo de senha" backups: title: "Backup acessos" @@ -36,15 +43,21 @@ pt-BR: edit: "Editar acesso" copy: "Copiar acesso" clone: "Clonar acesso" - show: "Mostar acesso" + show: "Mostrar acesso" new: "Novo acesso" list: "Busca" + project_not_exist: "Projeto com ID %{id} não existe" + project_id: "ID:" + btn: new: "Novo acesso" actions: "Ações" clipboard: "Copiar para área de transferência" + generate: "Gerar" find: "Procurar" + move: "Atribuir ao Projeto" + edit_tags: "Editar Marcadores" attr: lock: "Bloqueado" @@ -54,26 +67,38 @@ pt-BR: login: "Login" url: "URL" type: "Tipo" + project: "Projeto" tags: "Marcadores" file: "Arquivo de senha" comment: "Comentário" whitelist: "Membros" + created_at: "Criado em" + updated_at: "Alterado em" whitelist: selected_members: "Membros selecionados" available_members: "Membros disponíveis" + audit_log: + title: "Log de Auditoria" + action: "Ação" + user: "Usuário" + fields: "Campos Alterados" + timestamp: "Data/Hora" + no_entries: "Nenhum registro de auditoria" + ago: "atrás" + context_menu: copy_key: "Copiar senha" copy_url: "Copiar URL" copy_login: "Copiar login" delete: "Excluir acesso" - edit: "Excluir acesso" + edit: "Editar acesso" clone: "Clonar acesso" alert: key: - not_found: "Acesso não encotrado" + not_found: "Acesso não encontrado" confirm: key: @@ -95,5 +120,17 @@ pt-BR: key: not_set: "A chave de criptografia não está definida nas configurações" not_whitelisted: "Você não possuí permissão ao acesso" + length: "A chave de criptografia deve ter exatamente 16 símbolos" + not_orphaned: "Esta chave não está órfã" + project: + required: "Projeto é obrigatório" user: - not_allowed: "Você não tem permissão para visualizar essas senhas" \ No newline at end of file + not_allowed: "Você não tem permissão para visualizar essas senhas" + + permission_export_keys: "Exportar acessos" + permission_keys_all: "Visualizar todos os acessos" + permission_download_keys: "Baixar arquivos de acesso" + permission_view_keys: "Visualizar acessos" + permission_edit_keys: "Editar acessos" + permission_manage_whitelist_keys: "Gerenciar lista de acesso" + permission_whitelist_keys: "Lista de acesso" \ No newline at end of file diff --git a/config/locales/pt.yml b/config/locales/pt.yml index cbf0c2c..df1ba4f 100644 --- a/config/locales/pt.yml +++ b/config/locales/pt.yml @@ -1,18 +1,23 @@ # Portuguese - Portugal strings go here for Rails i18n pt: + button_view: "Ver" label_vault: 'Senhas' label_module: 'Chaves' + field_has_url: 'Tem URL' + field_has_login: 'Tem login' + field_body: 'Chave' settings: title: "Configuração de senhas" vault_title_encription_key: "Criptografia da chave" redmine_title_encription_key: "Use Criptografia Redmine" vault_encription_notice: "Usado apenas pelo método de criptografia do Vault" - redmine_encryption_notice: "Recomendado. Não esqueça de colocar sua chave em database_cipher_key em config / configuration.yml. IMPORTANTE: execute o rake redmine: plugins: vault: converte depois de mudar este interruptor." + redmine_encryption_notice: "Recomendado. Não esqueça de colocar sua chave em database_cipher_key em config/configuration.yml. IMPORTANTE: execute o rake redmine:plugins:vault:convert depois de mudar este interruptor." activerecord: models: password: "Chave" sftp: "SFTP" + key_file: "Arquivo de chave" backups: title: "Backup Chaves" @@ -37,15 +42,21 @@ pt: edit: "Editar Chave" copy: "Copiar Chave" clone: "Clonar Chave" - show: "Mostar Chave" + show: "Mostrar Chave" new: "Nova Chave" list: "Chaves" + project_not_exist: "Projeto com ID %{id} não existe" + project_id: "ID:" + btn: new: "Nova Chave" actions: "Ações" clipboard: "Copiar para clipboard" + generate: "Gerar" find: "Procurar" + move: "Atribuir ao Projeto" + edit_tags: "Editar Tags" attr: lock: "Bloqueado" @@ -55,15 +66,27 @@ pt: login: "Login" url: "URL" type: "Tipo" + project: "Projeto" tags: "Tags" file: "Chave em Arquivo" comment: "Comentário" whitelist: "Lista de Acesso" + created_at: "Criado" + updated_at: "Atualizado" whitelist: selected_members: "Selecionar Membros" available_members: "Membros disponíveis" + audit_log: + title: "Log de Auditoria" + action: "Ação" + user: "Utilizador" + fields: "Campos Alterados" + timestamp: "Data/Hora" + no_entries: "Sem registos de auditoria" + ago: "atrás" + context_menu: copy_key: "Copiar Chave" copy_url: "Copiar URL" @@ -74,7 +97,7 @@ pt: alert: key: - not_found: "Chave não encotrada" + not_found: "Chave não encontrada" confirm: key: @@ -82,7 +105,7 @@ pt: notice: settings: - saved: "Salvar Configurações" + saved: "Configurações guardadas" keys_restore: "Chaves restauradas" key: update: @@ -96,5 +119,17 @@ pt: key: not_set: "A chave de criptografia não está definida nas configurações" not_whitelisted: "Chave não permitida para você" + length: "A chave de criptografia deve ter exatamente 16 caracteres" + not_orphaned: "Esta chave não está órfã" + project: + required: "Projeto é obrigatório" user: not_allowed: "Você não tem permissão para visualizar essas senhas" + + permission_export_keys: "Exportar senhas" + permission_keys_all: "Ver todas as senhas" + permission_download_keys: "Descarregar ficheiros de senha" + permission_view_keys: "Ver senhas" + permission_edit_keys: "Editar senhas" + permission_manage_whitelist_keys: "Gerir lista de acesso" + permission_whitelist_keys: "Lista de acesso" diff --git a/config/locales/ru.yml b/config/locales/ru.yml index f35cd1b..85dc448 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -1,7 +1,11 @@ # Russian strings go here for Rails i18n ru: + button_view: "Просмотр" label_vault: 'Vault' label_module: 'Ключи' + field_has_url: 'Есть URL' + field_has_login: 'Есть логин' + field_body: 'Пароль' settings: title: "Настройки Vault" vault_title_encription_key: "Ключ шифрования" @@ -13,6 +17,7 @@ ru: models: password: "Ключ" sftp: "SFTP" + key_file: "Файл ключа" backups: title: "Управление резервными копиями" @@ -24,13 +29,13 @@ ru: export: title: pdf: "PDF" - pdf: - title: "ключи" + title: "Ключи" tag: - title: - popular: "Популярные теги" + title: + popular: "Популярные теги" + key: title: delete: "Удалить ключ" @@ -41,11 +46,17 @@ ru: new: "Новый ключ" list: "Ключи" + project_not_exist: "Проект с ID %{id} не существует" + project_id: "ID:" + btn: new: "Добавить ключ" actions: "Действия" clipboard: "Копировать в буфер" + generate: "Сгенерировать" find: "Искать" + move: "Назначить проекту" + edit_tags: "Редактировать теги" attr: lock: "Замок" @@ -55,22 +66,34 @@ ru: login: "Пользователь" url: "URL" type: "Тип" + project: "Проект" tags: "Теги" file: "Файл ключа" comment: "Комментарий" whitelist: "Список доступа" + created_at: "Создан" + updated_at: "Изменён" whitelist: selected_members: "Выбранные участники" available_members: "Доступные участники" + audit_log: + title: "Журнал аудита" + action: "Действие" + user: "Пользователь" + fields: "Изменённые поля" + timestamp: "Время" + no_entries: "Записей нет" + ago: "назад" + context_menu: - copy_key: "Копировать ключ" - copy_url: "Копировать URL" - copy_login: "Копировать логин" - delete: "Удалить ключ" - edit: "Редактировать ключ" - clone: "Клонировать ключ" + copy_key: "Копировать ключ" + copy_url: "Копировать URL" + copy_login: "Копировать логин" + delete: "Удалить ключ" + edit: "Редактировать ключ" + clone: "Клонировать ключ" alert: key: @@ -97,10 +120,14 @@ ru: not_set: "Ключ шифрования не установлен в настройках" not_whitelisted: "Ключ вам не доступен" length: "Ключ шифрования должен быть из 16 символов" + not_orphaned: "Этот ключ не является сиротой" + project: + required: "Проект обязателен" user: not_allowed: "Вам не разрешено просматривать эти пароли" permission_export_keys: "Экспортировать ключи" + permission_keys_all: "Просматривать все ключи" permission_download_keys: "Скачивать ключи" permission_view_keys: "Просматривать ключи" permission_edit_keys: "Редактировать ключи" diff --git a/config/locales/zh.yml b/config/locales/zh.yml index b709865..5b157bc 100644 --- a/config/locales/zh.yml +++ b/config/locales/zh.yml @@ -1,18 +1,23 @@ # Chinese strings go here for Rails i18n zh: + button_view: "查看" label_vault: 'Vault' label_module: '密钥' + field_has_url: '有URL' + field_has_login: '有登录名' + field_body: '密钥' settings: title: "Vault选项" vault_title_encription_key: "密钥" redmine_title_encription_key: "使用Redmine加密法" vault_encription_notice: "只用Vault加密法有效" - redmine_encryption_notice: "推荐选项。 请记得设置密钥database_cipher_key在config/configuration.yml文件。 注意: 修改这选项后,记得执行rake redmine:plugins:vault:convert。" + redmine_encryption_notice: "推荐选项。请记得设置密钥database_cipher_key在config/configuration.yml文件。注意: 修改这选项后,记得执行rake redmine:plugins:vault:convert。" activerecord: models: password: "密钥" sftp: "SFTP" + key_file: "密钥文件" backups: title: "密钥备份" @@ -24,13 +29,13 @@ zh: export: title: pdf: "PDF" - pdf: title: "密钥" tag: - title: - popular: "常用标签" + title: + popular: "常用标签" + key: title: delete: "删除密钥" @@ -41,11 +46,17 @@ zh: new: "新建密钥" list: "密钥" + project_not_exist: "ID为 %{id} 的项目不存在" + project_id: "ID:" + btn: new: "新建密钥" actions: "操作" - clipboard: "复制到剪贴板中" + clipboard: "复制到剪贴板" + generate: "生成" find: "搜索" + move: "分配到项目" + edit_tags: "编辑标签" attr: lock: "锁" @@ -55,22 +66,34 @@ zh: login: "用户名" url: "URL" type: "类型" + project: "项目" tags: "标签" file: "密钥文件" comment: "备注" whitelist: "访问表" + created_at: "创建时间" + updated_at: "更新时间" whitelist: selected_members: "已选成员" available_members: "可选成员" + audit_log: + title: "审计日志" + action: "操作" + user: "用户" + fields: "修改的字段" + timestamp: "时间戳" + no_entries: "无审计记录" + ago: "前" + context_menu: - copy_key: "复制密钥" - copy_url: "复制URL" - copy_login: "复制用户名" - delete: "删除密钥" - edit: "修改密钥" - clone: "克隆密钥" + copy_key: "复制密钥" + copy_url: "复制URL" + copy_login: "复制用户名" + delete: "删除密钥" + edit: "修改密钥" + clone: "克隆密钥" alert: key: @@ -86,7 +109,7 @@ zh: keys_restore: "密钥已恢复" key: update: - success: "成功修改密钥" + success: "成功修改密钥" delete: success: "成功删除密钥" create: @@ -94,7 +117,19 @@ zh: error: key: - not_set: "选项钥设置加密方法及密钥" - not_whitelisted: "关键不允许你" + not_set: "选项未设置加密密钥" + not_whitelisted: "您无权访问此密钥" + length: "加密密钥必须恰好为16个字符" + not_orphaned: "此密钥不是孤立密钥" + project: + required: "项目是必填项" user: not_allowed: "您无权查看这些密码" + + permission_export_keys: "导出密码" + permission_keys_all: "查看所有密码" + permission_download_keys: "下载密码文件" + permission_view_keys: "查看密码" + permission_edit_keys: "编辑密码" + permission_manage_whitelist_keys: "管理访问列表" + permission_whitelist_keys: "访问列表" diff --git a/init.rb b/init.rb index 87434d7..59dbc77 100755 --- a/init.rb +++ b/init.rb @@ -24,7 +24,7 @@ permission :whitelist_keys, keys: [ :index, :edit, :show ] end - menu :project_menu, :keys, { controller: 'keys', action: 'index' }, caption: Proc.new {I18n.t('label_module')}, after: :activity, param: :project_id + menu :project_menu, :keys, { controller: 'keys', action: 'index' }, caption: Proc.new {I18n.t('label_module')}, param: :project_id menu :top_menu, :keys, { controller: 'keys', action: 'all' }, caption: Proc.new {I18n.t('label_module')}, :if => Proc.new {User.current.allowed_to?({:controller => 'keys', :action => 'all'}, nil, :global => true)} settings :default => { diff --git a/lib/tasks/ciphering.rake b/lib/tasks/ciphering.rake index eaddf0e..7c7fcfc 100644 --- a/lib/tasks/ciphering.rake +++ b/lib/tasks/ciphering.rake @@ -21,7 +21,7 @@ namespace :redmine do code = if Setting.plugin_vault['use_redmine_encryption'] Encryptor.decrypt_all(Vault::Password,:body, engine: VaultCipher) && Encryptor.encrypt_all(Vault::Password,:body, engine: RedmineCipher) - else + else Encryptor.decrypt_all(Vault::Password,:body, engine: RedmineCipher) && Encryptor.encrypt_all(Vault::Password,:body, engine: VaultCipher) end @@ -34,7 +34,6 @@ namespace :redmine do raise "File does not save" end end - end end end diff --git a/test/functional/key_files_controller_test.rb b/test/functional/key_files_controller_test.rb index 981be5f..7f385d8 100644 --- a/test/functional/key_files_controller_test.rb +++ b/test/functional/key_files_controller_test.rb @@ -3,6 +3,7 @@ require 'byebug' class KeyFilesControllerTest < Vault::ControllerTest + tests KeysController fixtures :projects, :users, :roles, :members, :member_roles plugin_fixtures :keys, :vault_tags, :keys_vault_tags @@ -18,12 +19,12 @@ def setup def test_download_keyfile @request.session[:user_id] = 2 - get :download, project_id: 1, id: 3 + get :download, params: { project_id: 1, id: 3 } assert_response :success assert_equal 'application/octet-stream', response.content_type assert_equal "This is file for tests\n", response.body - assert_equal 'attachment; filename="ssh_access"', response.header["Content-Disposition"] + assert_match(/attachment; filename="ssh_access"/, response.header["Content-Disposition"]) end end diff --git a/test/functional/keys_controller_test.rb b/test/functional/keys_controller_test.rb index f416dde..01ea895 100644 --- a/test/functional/keys_controller_test.rb +++ b/test/functional/keys_controller_test.rb @@ -7,6 +7,8 @@ class KeysControllerTest < Vault::ControllerTest fixtures :projects, :users, :roles, :members, :member_roles plugin_fixtures :keys, :vault_tags, :keys_vault_tags + self.file_fixture_path = Rails.root.join('plugins', 'vault', 'test', 'fixtures') + def setup Role.find(1).add_permission! :view_keys Role.find(1).add_permission! :edit_keys @@ -16,7 +18,7 @@ def setup def test_index @request.session[:user_id] = 2 - get :index, project_id: 1 + get :index, params: { project_id: 1 } assert_not_nil assigns(:keys) assert_response :success @@ -26,7 +28,7 @@ def test_index def test_index_search @request.session[:user_id] = 2 - get :index, project_id: 1, query: '1' + get :index, params: { project_id: 1, search: '1' } assert_not_nil assigns(:keys) assert_equal 1, assigns(:keys).length @@ -37,7 +39,7 @@ def test_index_search def test_new @request.session[:user_id] = 2 - get :new, project_id: 1 + get :new, params: { project_id: 1 } assert_response :success assert_template 'new' @@ -46,14 +48,14 @@ def test_new def test_unpriv_index @request.session[:user_id] = 3 - get :index, project_id: 1 + get :index, params: { project_id: 1 } assert_response 403 end def test_unpriv_create @request.session[:user_id] = 3 - post :create, project_id: 1, key: { name: 'root', body: '123456' } + post :create, params: { project_id: 1, key: { name: 'root', body: '123456' } } assert_response 403 refute Vault::Key.exists?(name: 'root') @@ -61,14 +63,14 @@ def test_unpriv_create def test_unpriv_new @request.session[:user_id] = 3 - get :new, project_id: 1 + get :new, params: { project_id: 1 } assert_response 403 end def test_edit @request.session[:user_id] = 2 - get :edit, project_id: 1, id: 1 + get :edit, params: { project_id: 1, id: 1 } assert_response :success assert_template 'edit' @@ -78,26 +80,26 @@ def test_delete @request.session[:user_id] = 2 FileUtils.cp 'plugins/vault/test/fixtures/keyfile.txt', "#{Vault::KEYFILES_DIR}/server.key" - delete :destroy, project_id: 1, id: 3 + delete :destroy, params: { project_id: 1, id: 3 } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' key = Vault::Key.find_by_name('ssh_access') assert_nil key - refute File.exists?("#{Vault::KEYFILES_DIR}/server.key") + refute File.exist?("#{Vault::KEYFILES_DIR}/server.key") end def test_unpriv_edit @request.session[:user_id] = 3 - get :edit, project_id: 1, id: 1 + get :edit, params: { project_id: 1, id: 1 } assert_response 403 end def test_crossproject_edit @request.session[:user_id] = 2 - get :edit, project_id: 1, id: 2 + get :edit, params: { project_id: 1, id: 2 } assert_response :redirect end @@ -106,19 +108,19 @@ def test_update_with_new_file @request.session[:user_id] = 2 FileUtils.cp 'plugins/vault/test/fixtures/keyfile.txt', "#{Vault::KEYFILES_DIR}/server.key" - put :update, id: 3, project_id: 1, vault_key: {file: fixture_file_upload('../../plugins/vault/test/fixtures/keyfile.txt')} + put :update, params: { id: 3, project_id: 1, vault_key: { file: fixture_file_upload('keyfile.txt') } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' key = Vault::Key.find_by_name('ssh_access') refute_nil key - refute File.exists?("#{Vault::KEYFILES_DIR}/server.key") + refute File.exist?("#{Vault::KEYFILES_DIR}/server.key") end def test_update @request.session[:user_id] = 2 - put :update, id: 1, project_id: 1, vault_key: { name: 'database', login: 'me', type: 'Vault::KeyFile' } + put :update, params: { id: 1, project_id: 1, vault_key: { name: 'database', login: 'me', type: 'Vault::KeyFile' } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' @@ -134,19 +136,19 @@ def test_update def test_unpriv_update @request.session[:user_id] = 3 - put :update, id: 1, project_id: 1, vault_key: { name: 'database', body: '123456', login: 'me' } + put :update, params: { id: 1, project_id: 1, vault_key: { name: 'database', body: '123456', login: 'me' } } assert_response 403 end def test_crossproject_update @request.session[:user_id] = 2 - put :update, id: 2, project_id: 1, vault_key: { name: 'database', body: '123456', login: 'me' } + put :update, params: { id: 2, project_id: 1, vault_key: { name: 'database', body: '123456', login: 'me' } } assert_response :redirect end def test_show @request.session[:user_id] = 2 - get :show, project_id: 1, id: 1 + get :show, params: { project_id: 1, id: 1 } assert_response :success assert_template 'show' @@ -155,19 +157,19 @@ def test_show def test_crossproject_show @request.session[:user_id] = 2 - get :show, project_id: 1, id: 2 + get :show, params: { project_id: 1, id: 2 } assert_response :redirect end def test_unpriv_show @request.session[:user_id] = 3 - get :show, project_id: 1, id: 1 + get :show, params: { project_id: 1, id: 1 } assert_response 403 end def test_create @request.session[:user_id] = 2 - post :create, project_id: 1, vault_key: { name: 'database', type: 'Vault::Password', login: 'me', body: 123456 } + post :create, params: { project_id: 1, vault_key: { name: 'database', type: 'Vault::Password', login: 'me', body: 123456 } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' @@ -183,7 +185,7 @@ def test_create def test_create_with_tags @request.session[:user_id] = 2 - post :create, project_id: 1, vault_key: { name: 'router', type: 'Vault::Password', tags: "ssh, cisco" } + post :create, params: { project_id: 1, vault_key: { name: 'router', type: 'Vault::Password', tags: "ssh, cisco" } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' @@ -197,7 +199,7 @@ def test_create_with_tags def test_update_tags @request.session[:user_id] = 2 - put :update, id: 1, project_id: 1, vault_key: { tags: "mysql" } + put :update, params: { id: 1, project_id: 1, vault_key: { tags: "mysql" } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' @@ -210,21 +212,21 @@ def test_update_tags def test_tag_search @request.session[:user_id] = 2 - get :index, project_id: 1, query: "#ftp" + get :index, params: { project_id: 1, search: "#ftp" } assert_response :success assert_template 'index' keys = assigns(:keys) assert_not_nil keys - assert_equal 1, keys.count + assert_equal 1, keys.count assert_equal Set.new(['ftp', 'ssh']), Set.new(keys[0].tags.map(&:name)) end def test_upload_keyfile @request.session[:user_id] = 2 - post :create, project_id: 1, vault_key: { name: 'database', type: 'Vault::KeyFile', file: fixture_file_upload('../../plugins/vault/test/fixtures/keyfile.txt') } + post :create, params: { project_id: 1, vault_key: { name: 'database', type: 'Vault::KeyFile', file: fixture_file_upload('keyfile.txt') } } assert_response :redirect assert_redirected_to '/projects/ecookbook/keys' @@ -232,7 +234,7 @@ def test_upload_keyfile key = Vault::Key.find_by_name('database') key.decrypt! assert_equal 'Vault::KeyFile', key.type - assert File.exists?("#{Vault::KEYFILES_DIR}/#{key.file}"), "File: #{key.file} should be at Rails root keyfiles dir" + assert File.exist?("#{Vault::KEYFILES_DIR}/#{key.file}"), "File: #{key.file} should be at Rails root keyfiles dir" end end diff --git a/test/unit/locale_test.rb b/test/unit/locale_test.rb new file mode 100644 index 0000000..b7c7076 --- /dev/null +++ b/test/unit/locale_test.rb @@ -0,0 +1,49 @@ +require File.expand_path('../../test_helper', __FILE__) +require 'yaml' + +class LocaleTest < ActiveSupport::TestCase + + LOCALES_DIR = File.expand_path('../../../config/locales', __FILE__) + REFERENCE_LOCALE = 'en' + + def self.flatten_keys(hash, prefix = '') + hash.each_with_object([]) do |(k, v), keys| + full_key = prefix.empty? ? k.to_s : "#{prefix}.#{k}" + if v.is_a?(Hash) + keys.concat(flatten_keys(v, full_key)) + else + keys << full_key + end + end + end + + def reference_keys + @reference_keys ||= begin + data = YAML.load_file(File.join(LOCALES_DIR, "#{REFERENCE_LOCALE}.yml")) + self.class.flatten_keys(data[REFERENCE_LOCALE]) + end + end + + def locale_files + Dir.glob(File.join(LOCALES_DIR, '*.yml')).reject do |f| + File.basename(f, '.yml') == REFERENCE_LOCALE + end + end + + def test_all_locales_have_reference_keys + missing = {} + + locale_files.each do |file| + locale = File.basename(file, '.yml') + data = YAML.load_file(file) + locale_keys = self.class.flatten_keys(data[locale] || {}) + absent = reference_keys - locale_keys + missing[locale] = absent unless absent.empty? + end + + assert missing.empty?, + "Locale(s) missing keys from #{REFERENCE_LOCALE}.yml:\n" + + missing.map { |locale, keys| " #{locale}: #{keys.join(', ')}" }.join("\n") + end + +end