diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-10 03:06:44 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2019-10-10 03:06:44 +0300 |
commit | 308146dc398fd4c13453048105498018459e0985 (patch) | |
tree | d843eb63c1672e4b18c483907e2cd4aa7fca708e /app | |
parent | 4b28d5ae770c6bd332283a3f13ceae06329c409b (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
24 files changed, 463 insertions, 133 deletions
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue index e8905b479ee..78aaa9df0ec 100644 --- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue +++ b/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue @@ -1,37 +1,46 @@ <script> import _ from 'underscore'; -import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue'; +import { GlModal, GlButton, GlFormInput } from '@gitlab/ui'; import { s__, sprintf } from '~/locale'; export default { components: { - DeprecatedModal, + GlModal, + GlButton, + GlFormInput, }, props: { + title: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + action: { + type: String, + required: true, + }, + secondaryAction: { + type: String, + required: true, + }, deleteUserUrl: { type: String, - required: false, - default: '', + required: true, }, blockUserUrl: { type: String, - required: false, - default: '', - }, - deleteContributions: { - type: Boolean, - required: false, - default: false, + required: true, }, username: { type: String, - required: false, - default: '', + required: true, }, csrfToken: { type: String, - required: false, - default: '', + required: true, }, }, data() { @@ -40,32 +49,12 @@ export default { }; }, computed: { - title() { - const keepContributionsTitle = s__('AdminUsers|Delete User %{username}?'); - const deleteContributionsTitle = s__('AdminUsers|Delete User %{username} and contributions?'); - - return sprintf( - this.deleteContributions ? deleteContributionsTitle : keepContributionsTitle, - { - username: `'${_.escape(this.username)}'`, - }, - false, - ); + modalTitle() { + return sprintf(this.title, { username: this.username }); }, text() { - const keepContributionsText = s__(`AdminArea| - You are about to permanently delete the user %{username}. - Issues, merge requests, and groups linked to them will be transferred to a system-wide "Ghost-user". - To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. - Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); - - const deleteContributionsText = s__(`AdminArea| - You are about to permanently delete the user %{username}. - This will delete all of the issues, merge requests, and groups linked to them. - To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. - Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered.`); return sprintf( - this.deleteContributions ? deleteContributionsText : keepContributionsText, + this.content, { username: `<strong>${_.escape(this.username)}</strong>`, strong_start: '<strong>', @@ -83,12 +72,7 @@ export default { false, ); }, - primaryButtonLabel() { - const keepContributionsLabel = s__('AdminUsers|Delete user'); - const deleteContributionsLabel = s__('AdminUsers|Delete user and contributions'); - return this.deleteContributions ? deleteContributionsLabel : keepContributionsLabel; - }, secondaryButtonLabel() { return s__('AdminUsers|Block user'); }, @@ -97,8 +81,12 @@ export default { }, }, methods: { + show() { + this.$refs.modal.show(); + }, onCancel() { this.enteredUsername = ''; + this.$refs.modal.hide(); }, onSecondaryAction() { const { form } = this.$refs; @@ -117,43 +105,28 @@ export default { </script> <template> - <deprecated-modal - id="delete-user-modal" - :title="title" - :text="text" - :primary-button-label="primaryButtonLabel" - :secondary-button-label="secondaryButtonLabel" - :submit-disabled="!canSubmit" - kind="danger" - @submit="onSubmit" - @cancel="onCancel" - > - <template slot="body" slot-scope="props"> - <p v-html="props.text"></p> + <gl-modal ref="modal" modal-id="delete-user-modal" :title="modalTitle" kind="danger"> + <template> + <p v-html="text"></p> <p v-html="confirmationTextLabel"></p> <form ref="form" :action="deleteUserUrl" method="post"> <input ref="method" type="hidden" name="_method" value="delete" /> <input :value="csrfToken" type="hidden" name="authenticity_token" /> - <input + <gl-form-input v-model="enteredUsername" + autofocus type="text" name="username" - class="form-control" - aria-labelledby="input-label" autocomplete="off" /> </form> </template> - <template slot="secondary-button"> - <button - :disabled="!canSubmit" - type="button" - class="btn js-secondary-button btn-warning" - data-dismiss="modal" - @click="onSecondaryAction" - > - {{ secondaryButtonLabel }} - </button> + <template slot="modal-footer"> + <gl-button variant="secondary" @click="onCancel">{{ s__('Cancel') }}</gl-button> + <gl-button :disabled="!canSubmit" variant="warning" @click="onSecondaryAction"> + {{ secondaryAction }} + </gl-button> + <gl-button :disabled="!canSubmit" variant="danger" @click="onSubmit">{{ action }}</gl-button> </template> - </deprecated-modal> + </gl-modal> </template> diff --git a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue new file mode 100644 index 00000000000..a08d32028c3 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue @@ -0,0 +1,77 @@ +<script> +export default { + props: { + modalConfiguration: { + required: true, + type: Object, + }, + actionModals: { + required: true, + type: Object, + }, + csrfToken: { + required: true, + type: String, + }, + }, + data() { + return { + currentModalData: null, + }; + }, + computed: { + activeModal() { + if (!this.currentModalData) return null; + const { glModalAction: action } = this.currentModalData; + + return this.actionModals[action]; + }, + + modalProps() { + const { glModalAction: requestedAction } = this.currentModalData; + return { + ...this.modalConfiguration[requestedAction], + ...this.currentModalData, + csrfToken: this.csrfToken, + }; + }, + }, + + mounted() { + document.addEventListener('click', this.handleClick); + }, + + beforeDestroy() { + document.removeEventListener('click', this.handleClick); + }, + + methods: { + handleClick(e) { + const { glModalAction: action } = e.target.dataset; + if (!action) return; + + this.show(e.target.dataset); + e.preventDefault(); + }, + + show(modalData) { + const { glModalAction: requestedAction } = modalData; + if (!this.actionModals[requestedAction]) { + throw new Error(`Requested non-existing modal action ${requestedAction}`); + } + if (!this.modalConfiguration[requestedAction]) { + throw new Error(`Modal action ${requestedAction} has no configuration in HTML`); + } + + this.currentModalData = modalData; + + return this.$nextTick().then(() => { + this.$refs.modal.show(); + }); + }, + }, +}; +</script> +<template> + <div :is="activeModal" v-if="activeModal" ref="modal" v-bind="modalProps" /> +</template> diff --git a/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue new file mode 100644 index 00000000000..4c335cfb018 --- /dev/null +++ b/app/assets/javascripts/pages/admin/users/components/user_operation_confirmation_modal.vue @@ -0,0 +1,70 @@ +<script> +import { GlModal } from '@gitlab/ui'; +import { sprintf } from '~/locale'; + +export default { + components: { + GlModal, + }, + props: { + title: { + type: String, + required: true, + }, + content: { + type: String, + required: true, + }, + action: { + type: String, + required: true, + }, + url: { + type: String, + required: true, + }, + username: { + type: String, + required: true, + }, + csrfToken: { + type: String, + required: true, + }, + method: { + type: String, + required: false, + default: 'put', + }, + }, + computed: { + modalTitle() { + return sprintf(this.title, { username: this.username }); + }, + }, + methods: { + show() { + this.$refs.modal.show(); + }, + submit() { + this.$refs.form.submit(); + }, + }, +}; +</script> +<template> + <gl-modal + ref="modal" + modal-id="user-operation-modal" + :title="modalTitle" + ok-variant="warning" + :ok-title="action" + @ok="submit" + > + <form ref="form" :action="url" method="post"> + <span v-html="content"></span> + <input ref="method" type="hidden" name="_method" :value="method" /> + <input :value="csrfToken" type="hidden" name="authenticity_token" /> + </form> + </gl-modal> +</template> diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js index 45046688b57..bc96e88351b 100644 --- a/app/assets/javascripts/pages/admin/users/index.js +++ b/app/assets/javascripts/pages/admin/users/index.js @@ -1,46 +1,65 @@ -import $ from 'jquery'; import Vue from 'vue'; import Translate from '~/vue_shared/translate'; +import ModalManager from './components/user_modal_manager.vue'; +import DeleteUserModal from './components/delete_user_modal.vue'; +import UserOperationConfirmationModal from './components/user_operation_confirmation_modal.vue'; import csrf from '~/lib/utils/csrf'; -import deleteUserModal from './components/delete_user_modal.vue'; +const MODAL_TEXTS_CONTAINER_SELECTOR = '#modal-texts'; +const MODAL_MANAGER_SELECTOR = '#user-modal'; +const ACTION_MODALS = { + deactivate: UserOperationConfirmationModal, + block: UserOperationConfirmationModal, + delete: DeleteUserModal, + 'delete-with-contributions': DeleteUserModal, +}; + +function loadModalsConfigurationFromHtml(modalsElement) { + const modalsConfiguration = {}; + + if (!modalsElement) { + /* eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings */ + throw new Error('Modals content element not found!'); + } + + Array.from(modalsElement.children).forEach(node => { + const { modal, ...config } = node.dataset; + modalsConfiguration[modal] = { + title: node.dataset.title, + ...config, + content: node.innerHTML, + }; + }); + + return modalsConfiguration; +} document.addEventListener('DOMContentLoaded', () => { Vue.use(Translate); - const deleteUserModalEl = document.getElementById('delete-user-modal'); + const modalConfiguration = loadModalsConfigurationFromHtml( + document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR), + ); - const deleteModal = new Vue({ - el: deleteUserModalEl, - data: { - deleteUserUrl: '', - blockUserUrl: '', - deleteContributions: '', - username: '', + // eslint-disable-next-line no-new + new Vue({ + el: MODAL_MANAGER_SELECTOR, + functional: true, + methods: { + show(...args) { + this.$refs.manager.show(...args); + }, }, - render(createElement) { - return createElement(deleteUserModal, { + render(h) { + return h(ModalManager, { + ref: 'manager', props: { - deleteUserUrl: this.deleteUserUrl, - blockUserUrl: this.blockUserUrl, - deleteContributions: this.deleteContributions, - username: this.username, + modalConfiguration, + actionModals: ACTION_MODALS, csrfToken: csrf.token, }, }); }, }); - - $(document).on('shown.bs.modal', event => { - if (event.relatedTarget.classList.contains('delete-user-button')) { - const buttonProps = event.relatedTarget.dataset; - deleteModal.deleteUserUrl = buttonProps.deleteUserUrl; - deleteModal.blockUserUrl = buttonProps.blockUserUrl; - deleteModal.deleteContributions = event.relatedTarget.hasAttribute( - 'data-delete-contributions', - ); - deleteModal.username = buttonProps.username; - } - }); }); diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb index 61d36d1efc2..4c1ac8f206a 100644 --- a/app/controllers/admin/users_controller.rb +++ b/app/controllers/admin/users_controller.rb @@ -58,6 +58,22 @@ class Admin::UsersController < Admin::ApplicationController end end + def activate + return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated")) if user.blocked? + + user.activate + redirect_back_or_admin_user(notice: _("Successfully activated")) + end + + def deactivate + return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated")) if user.blocked? + return redirect_back_or_admin_user(notice: _("Successfully deactivated")) if user.deactivated? + return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: ::User::MINIMUM_INACTIVE_DAYS }) unless user.can_be_deactivated? + + user.deactivate + redirect_back_or_admin_user(notice: _("Successfully deactivated")) + end + def block if update_user { |user| user.block } redirect_back_or_admin_user(notice: _("Successfully blocked")) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 01d80d77080..f5939b61948 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -26,6 +26,7 @@ class ApplicationController < ActionController::Base before_action :add_gon_variables, unless: [:peek_request?, :json_request?] before_action :configure_permitted_parameters, if: :devise_controller? before_action :require_email, unless: :devise_controller? + before_action :active_user_check, unless: :devise_controller? before_action :set_usage_stats_consent_flag before_action :check_impersonation_availability @@ -294,6 +295,14 @@ class ApplicationController < ActionController::Base end end + def active_user_check + return unless current_user && current_user.deactivated? + + sign_out current_user + flash[:alert] = _("Your account has been deactivated by your administrator. Please log back in to reactivate your account.") + redirect_to new_user_session_path + end + def ldap_security_check if current_user && current_user.requires_ldap_check? return unless current_user.try_obtain_ldap_lease diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index 755ce3463c4..b992972dfb8 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -148,6 +148,11 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController if user.two_factor_enabled? && !auth_user.bypass_two_factor? prompt_for_two_factor(user) else + if user.deactivated? + user.activate + flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.') + end + sign_in_and_redirect(user, event: :authentication) end else diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb index 23ef9157363..7b694dcdd77 100644 --- a/app/controllers/projects/merge_requests/diffs_controller.rb +++ b/app/controllers/projects/merge_requests/diffs_controller.rb @@ -25,6 +25,9 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic return render_404 unless diffable diffs = diffable.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options) + positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user) + + diffs.unfold_diff_files(positions.unfoldable) options = { merge_request: @merge_request, diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb index f8da152e3d2..1c506065b56 100644 --- a/app/controllers/sessions_controller.rb +++ b/app/controllers/sessions_controller.rb @@ -57,8 +57,14 @@ class SessionsController < Devise::SessionsController reset_password_sent_at: nil) end - # hide the signed-in notification - flash[:notice] = nil + if resource.deactivated? + resource.activate + flash[:notice] = _('Welcome back! Your account had been deactivated due to inactivity but is now reactivated.') + else + # hide the default signed-in notification + flash[:notice] = nil + end + log_audit_event(current_user, resource, with: authentication_method) log_user_activity(current_user) end diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb index 556be4c4338..1dd1a27437e 100644 --- a/app/finders/user_finder.rb +++ b/app/finders/user_finder.rb @@ -52,6 +52,12 @@ class UserFinder end end + def find_by_ssh_key_id + return unless input_is_id? + + User.find_by_ssh_key_id(@username_or_id) + end + def input_is_id? @username_or_id.is_a?(Numeric) || @username_or_id =~ /^\d+$/ end diff --git a/app/models/concerns/ci/pipeline_delegator.rb b/app/models/concerns/ci/pipeline_delegator.rb index dbc5ed1bc9a..76e0cbc7dff 100644 --- a/app/models/concerns/ci/pipeline_delegator.rb +++ b/app/models/concerns/ci/pipeline_delegator.rb @@ -15,7 +15,8 @@ module Ci :merge_request_ref?, :source_ref, :source_ref_slug, - :legacy_detached_merge_request_pipeline?, to: :pipeline + :legacy_detached_merge_request_pipeline?, + :merge_train_pipeline?, to: :pipeline end end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 2b6934d4c83..50efe5d6aa0 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -454,6 +454,15 @@ class MergeRequest < ApplicationRecord merge_request_diffs.where.not(id: merge_request_diff.id) end + # Overwritten in EE + def note_positions_for_paths(paths, _user = nil) + positions = notes.new_diff_notes.joins(:note_diff_file) + .where('note_diff_files.old_path IN (?) OR note_diff_files.new_path IN (?)', paths, paths) + .positions + + Gitlab::Diff::PositionCollection.new(positions, diff_head_sha) + end + def preloads_discussion_diff_highlighting? true end diff --git a/app/models/note.rb b/app/models/note.rb index 03a38346731..edc4a332581 100644 --- a/app/models/note.rb +++ b/app/models/note.rb @@ -193,6 +193,12 @@ class Note < ApplicationRecord groups end + def positions + where.not(position: nil) + .select(:id, :type, :position) # ActiveRecord needs id and type for typecasting. + .map(&:position) + end + def count_for_collection(ids, type) user.select('noteable_id', 'COUNT(*) as count') .group(:noteable_id) diff --git a/app/models/user.rb b/app/models/user.rb index ad56d7a32c5..c4075f06dff 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -59,6 +59,8 @@ class User < ApplicationRecord # Removed in GitLab 12.3. Keep until after 2019-09-22. self.ignored_columns += %i[support_bot] + MINIMUM_INACTIVE_DAYS = 14 + # Override Devise::Models::Trackable#update_tracked_fields! # to limit database writes to at most once every hour # rubocop: disable CodeReuse/ServiceClass @@ -242,18 +244,25 @@ class User < ApplicationRecord state_machine :state, initial: :active do event :block do transition active: :blocked + transition deactivated: :blocked transition ldap_blocked: :blocked end event :ldap_block do transition active: :ldap_blocked + transition deactivated: :ldap_blocked end event :activate do + transition deactivated: :active transition blocked: :active transition ldap_blocked: :active end + event :deactivate do + transition active: :deactivated + end + state :blocked, :ldap_blocked do def blocked? true @@ -284,6 +293,7 @@ class User < ApplicationRecord scope :blocked, -> { with_states(:blocked, :ldap_blocked) } scope :external, -> { where(external: true) } scope :active, -> { with_state(:active).non_internal } + scope :deactivated, -> { with_state(:deactivated).non_internal } scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) } scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) } @@ -431,6 +441,8 @@ class User < ApplicationRecord without_projects when 'external' external + when 'deactivated' + deactivated else active end @@ -521,7 +533,7 @@ class User < ApplicationRecord # Returns a user for the given SSH key. def find_by_ssh_key_id(key_id) - Key.find_by(id: key_id)&.user + find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id)) end def find_by_full_path(path, follow_redirects: false) @@ -1534,6 +1546,17 @@ class User < ApplicationRecord !!(password_expires_at && password_expires_at < Time.now) end + def can_be_deactivated? + active? && no_recent_activity? + end + + def last_active_at + last_activity = last_activity_on&.to_time&.in_time_zone + last_sign_in = current_sign_in_at + + [last_activity, last_sign_in].compact.max + end + # @deprecated alias_method :owned_or_masters_groups, :owned_or_maintainers_groups @@ -1683,6 +1706,10 @@ class User < ApplicationRecord ::Group.where(id: developer_groups_hierarchy.select(:id), project_creation_level: project_creation_levels) end + + def no_recent_activity? + last_active_at.to_i <= MINIMUM_INACTIVE_DAYS.days.ago.to_i + end end User.prepend_if_ee('EE::User') diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb index 2305c55033f..18c23cbd13a 100644 --- a/app/policies/base_policy.rb +++ b/app/policies/base_policy.rb @@ -17,6 +17,10 @@ class BasePolicy < DeclarativePolicy::Base with_options scope: :user, score: 0 condition(:blocked) { @user&.blocked? } + desc "User is deactivated" + with_options scope: :user, score: 0 + condition(:deactivated) { @user&.deactivated? } + desc "User has access to all private groups & projects" with_options scope: :user, score: 0 condition(:full_private_access) { @user&.full_private_access? } diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb index 659fde574fc..4d66a9e7d67 100644 --- a/app/policies/global_policy.rb +++ b/app/policies/global_policy.rb @@ -44,6 +44,12 @@ class GlobalPolicy < BasePolicy prevent :use_slash_commands end + rule { deactivated }.policy do + prevent :access_git + prevent :access_api + prevent :receive_notifications + end + rule { required_terms_not_accepted }.policy do prevent :access_api prevent :access_git diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml index b53b2019a97..a218885a00e 100644 --- a/app/views/admin/users/_head.html.haml +++ b/app/views/admin/users/_head.html.haml @@ -6,6 +6,8 @@ %span.cred (Internal) - if @user.admin %span.cred (Admin) + - if @user.deactivated? + %span.cred (Deactivated) = render_if_exists 'admin/users/audtior_user_badge' .float-right diff --git a/app/views/admin/users/_modals.html.haml b/app/views/admin/users/_modals.html.haml new file mode 100644 index 00000000000..eaec6d69f5a --- /dev/null +++ b/app/views/admin/users/_modals.html.haml @@ -0,0 +1,30 @@ +#user-modal +#modal-texts.hidden{ "hidden": true, "aria-hidden": true } + %div{ data: { modal: "deactivate", + title: s_("AdminUsers|Deactivate User %{username}?"), + action: s_("AdminUsers|Deactivate") } } + = render partial: 'admin/users/user_deactivation_effects' + + %div{ data: { modal: "block", + title: s_("AdminUsers|Block user %{username}?"), + action: s_("AdminUsers|Block") } } + = render partial: 'admin/users/user_block_effects' + + %div{ data: { modal: "delete", + title: s_("AdminUsers|Delete User %{username}?"), + action: s_('AdminUsers|Delete user'), + 'secondary-action': s_('AdminUsers|Block user') } } + = s_('AdminUsers|You are about to permanently delete the user %{username}. Issues, merge requests, + and groups linked to them will be transferred to a system-wide "Ghost-user". To avoid data loss, + consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, + it cannot be undone or recovered.') + + %div{ data: { modal: "delete-with-contributions", + title: s_("AdminUsers|Delete User %{username} and contributions?"), + action: s_('AdminUsers|Delete user and contributions') , + 'secondary-action': s_('AdminUsers|Block user') } } + = s_('AdminUsers|You are about to permanently delete the user %{username}. This will delete all of the issues, + merge requests, and groups linked to them. To avoid data loss, + consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, + it cannot be undone or recovered.') + diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml index 90a056dfe30..ca5109614fc 100644 --- a/app/views/admin/users/_user.html.haml +++ b/app/views/admin/users/_user.html.haml @@ -31,7 +31,19 @@ - elsif user.blocked? = link_to _('Unblock'), unblock_admin_user_path(user), method: :put - else - = link_to _('Block'), block_admin_user_path(user), data: { confirm: "#{s_('AdminUsers|User will be blocked').upcase}! #{_('Are you sure')}?" }, method: :put + %button.btn{ data: { 'gl-modal-action': 'block', + url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Block') + - if user.can_be_deactivated? + %li + %button.btn{ data: { 'gl-modal-action': 'deactivate', + url: deactivate_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Deactivate') + - elsif user.deactivated? + %li + = link_to _('Activate'), activate_admin_user_path(user), method: :put - if user.access_locked? %li = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') } @@ -39,19 +51,14 @@ %li.divider - if user.can_be_removed? %li - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', - target: '#delete-user-modal', + %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete', delete_user_url: admin_user_path(user), block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name), - delete_contributions: false }, type: 'button' } + username: sanitize_name(user.name) } } = s_('AdminUsers|Delete user') - - %li - %button.delete-user-button.btn.text-danger{ data: { toggle: 'modal', - target: '#delete-user-modal', - delete_user_url: admin_user_path(user, hard_delete: true), - block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name), - delete_contributions: true }, type: 'button' } - = s_('AdminUsers|Delete user and contributions') + %li + %button.delete-user-button.btn.text-danger{ data: { 'gl-modal-action': 'delete-with-contributions', + delete_user_url: admin_user_path(user, hard_delete: true), + block_user_url: block_admin_user_path(user), + username: sanitize_name(user.name) } } + = s_('AdminUsers|Delete user and contributions') diff --git a/app/views/admin/users/_user_activation_effects.html.haml b/app/views/admin/users/_user_activation_effects.html.haml new file mode 100644 index 00000000000..244836dac11 --- /dev/null +++ b/app/views/admin/users/_user_activation_effects.html.haml @@ -0,0 +1,6 @@ +%p + = s_('AdminUsers|Reactivating a user will:') +%ul + %li + = s_('AdminUsers|Restore user access to the account, including web, Git and API.') + = render_if_exists 'admin/users/user_activation_effects_on_seats' diff --git a/app/views/admin/users/_user_block_effects.html.haml b/app/views/admin/users/_user_block_effects.html.haml new file mode 100644 index 00000000000..8ffbe145169 --- /dev/null +++ b/app/views/admin/users/_user_block_effects.html.haml @@ -0,0 +1,11 @@ +%p + = s_('AdminUsers|Blocking user has the following effects:') +%ul + %li + = s_('AdminUsers|User will not be able to login') + %li + = s_('AdminUsers|User will not be able to access git repositories') + %li + = s_('AdminUsers|Personal projects will be left') + %li + = s_('AdminUsers|Owned groups will be left') diff --git a/app/views/admin/users/_user_deactivation_effects.html.haml b/app/views/admin/users/_user_deactivation_effects.html.haml new file mode 100644 index 00000000000..6cc47214d77 --- /dev/null +++ b/app/views/admin/users/_user_deactivation_effects.html.haml @@ -0,0 +1,16 @@ +%p + = s_('AdminUsers|Deactivating a user has the following effects:') +%ul + %li + = s_('AdminUsers|The user will be logged out') + %li + = s_('AdminUsers|The user will not be able to access git repositories') + %li + = s_('AdminUsers|The user will not be able to access the API') + %li + = s_('AdminUsers|The user will not receive any notifications') + %li + = s_('AdminUsers|When the user logs back in, their account will reactivate as a fully active account') + %li + = s_('AdminUsers|Personal projects, group and user history will be left intact') + = render_if_exists 'admin/users/user_deactivation_effects_on_seats' diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml index 9ce16749945..3c6ad899d1e 100644 --- a/app/views/admin/users/index.html.haml +++ b/app/views/admin/users/index.html.haml @@ -30,6 +30,10 @@ = link_to admin_users_path(filter: "blocked") do = s_('AdminUsers|Blocked') %small.badge.badge-pill= limited_counter_with_delimiter(User.blocked) + = nav_link(html_options: { class: active_when(params[:filter] == 'deactivated') }) do + = link_to admin_users_path(filter: "deactivated") do + = s_('AdminUsers|Deactivated') + %small.badge.badge-pill= limited_counter_with_delimiter(User.deactivated) = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do = link_to admin_users_path(filter: "wop") do = s_('AdminUsers|Without projects') @@ -50,6 +54,7 @@ = icon("search", class: "search-icon") = button_tag s_('AdminUsers|Search users') if Rails.env.test? .dropdown.user-sort-dropdown + = label_tag 'Sort by', nil, class: 'label-bold' - toggle_text = @sort.present? ? users_sort_options_hash[@sort] : sort_title_name = dropdown_toggle(toggle_text, { toggle: 'dropdown' }) %ul.dropdown-menu.dropdown-menu-right @@ -74,4 +79,4 @@ = paginate @users, theme: "gitlab" -#delete-user-modal += render partial: 'admin/users/modals' diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml index a988f746ced..706fa033c51 100644 --- a/app/views/admin/users/show.html.haml +++ b/app/views/admin/users/show.html.haml @@ -156,6 +156,27 @@ = render_if_exists 'admin/users/user_detail_note' + - if @user.deactivated? + .card.border-info + .card-header.bg-info.text-white + Reactivate this user + .card-body + = render partial: 'admin/users/user_activation_effects' + %br + = link_to 'Activate user', activate_admin_user_path(@user), method: :put, class: "btn btn-info", data: { confirm: 'Are you sure?' } + - elsif @user.can_be_deactivated? + .card.border-warning + .card-header.bg-warning.text-white + Deactivate this user + .card-body + = render partial: 'admin/users/user_deactivation_effects' + %br + %button.btn.btn-warning{ data: { 'gl-modal-action': 'deactivate', + content: 'You can always re-activate their account, their data will remain intact.', + url: deactivate_admin_user_path(@user), + username: sanitize_name(@user.name) } } + = s_('AdminUsers|Deactivate user') + - if @user.blocked? .card.border-info .card-header.bg-info.text-white @@ -172,14 +193,13 @@ .card-header.bg-warning.text-white Block this user .card-body - %p Blocking user has the following effects: - %ul - %li User will not be able to login - %li User will not be able to access git repositories - %li Personal projects will be left - %li Owned groups will be left + = render partial: 'admin/users/user_block_effects' %br - = link_to 'Block user', block_admin_user_path(@user), data: { confirm: 'USER WILL BE BLOCKED! Are you sure?' }, method: :put, class: "btn btn-warning" + %button.btn.btn-warning{ data: { 'gl-modal-action': 'block', + content: 'You can always unblock their account, their data will remain intact.', + url: block_admin_user_path(@user), + username: sanitize_name(@user.name) } } + = s_('AdminUsers|Block user') - if @user.access_locked? .card.border-info .card-header.bg-info.text-white @@ -197,12 +217,10 @@ %p Deleting a user has the following effects: = render 'users/deletion_guidance', user: @user %br - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', - target: '#delete-user-modal', + %button.delete-user-button.btn.btn-danger{ data: { 'gl-modal-action': 'delete', delete_user_url: admin_user_path(@user), block_user_url: block_admin_user_path(@user), - username: @user.name, - delete_contributions: false }, type: 'button' } + username: sanitize_name(@user.name) } } = s_('AdminUsers|Delete user') - else - if @user.solo_owned_groups.present? @@ -229,15 +247,13 @@ the user, and projects in them, will also be removed. Commits to other projects are unaffected. %br - %button.delete-user-button.btn.btn-danger{ data: { toggle: 'modal', - target: '#delete-user-modal', + %button.delete-user-button.btn.btn-danger{ data: { 'gl-modal-action': 'delete-with-contributions', delete_user_url: admin_user_path(@user, hard_delete: true), block_user_url: block_admin_user_path(@user), - username: @user.name, - delete_contributions: true }, type: 'button' } + username: @user.name } } = s_('AdminUsers|Delete user and contributions') - else %p You don't have access to delete this user. - #delete-user-modal += render partial: 'admin/users/modals' |