From 6812cd311459f1f176a24e8814765a07c3eb9187 Mon Sep 17 00:00:00 2001
From: Manoj M J <mmj@gitlab.com>
Date: Fri, 2 Feb 2024 11:08:32 +0000
Subject: [PATCH] Move JavaScript to accommodate URL changes

Changing `/-/profile` -> `/-/user_settings/profile`
---
 .../style/hash_as_last_array_item.yml         |   1 +
 .../javascripts/pages/profiles/index.js       |   9 +-
 .../profiles/show/index.js                    |  14 +-
 .../profiles/show}/init_timezone_dropdown.js  |   0
 .../password_prompt/constants.js              |   0
 .../password_prompt/index.js                  |   0
 .../password_prompt/password_prompt_modal.vue |   0
 app/controllers/application_controller.rb     |   2 +-
 .../concerns/confirm_email_warning.rb         |   2 +-
 .../oauth/applications_controller.rb          |   2 +-
 .../profiles/avatars_controller.rb            |   2 +-
 app/controllers/profiles_controller.rb        |  28 +---
 .../user_settings/profiles_controller.rb      |  78 ++++++++++
 app/helpers/profiles_helper.rb                |   2 +-
 app/helpers/search_helper.rb                  |   2 +-
 app/helpers/sidebars_helper.rb                |   4 +-
 app/helpers/tree_helper.rb                    |   2 +-
 app/presenters/user_presenter.rb              |   2 +-
 app/views/layouts/profile.html.haml           |   2 +-
 app/views/profiles/emails/index.html.haml     |   2 +-
 app/views/profiles/slacks/edit.html.haml      |   2 +-
 .../projects/merge_requests/_widget.html.haml |   2 +-
 app/views/shared/_no_password.html.haml       |   2 +-
 app/views/shared/_no_ssh.html.haml            |   2 +-
 app/views/shared/_project_limit.html.haml     |   2 +-
 .../profiles/_email_settings.html.haml        |   0
 .../profiles/_name.html.haml                  |   0
 .../profiles/show.html.haml                   |   6 +-
 app/views/users/show.html.haml                |   2 +-
 config/routes/profile.rb                      |   2 +-
 config/routes/user_settings.rb                |   5 +
 .../project/integrations/webhook_events.md    |   2 +-
 .../vue_shared/purchase_flow/constants.js     |   2 +-
 .../views/profiles/_email_settings.html.haml  |   3 -
 .../profiles/_email_settings.html.haml        |   3 +
 .../profiles/_name.html.haml                  |   0
 .../profiles_controller_spec.rb               |  18 +--
 .../features/profiles/usage_quotas_spec.rb    |   2 +-
 .../profiles/user_visits_profile_spec.rb      |   4 +-
 .../features/security/profile_access_spec.rb  |   4 +-
 ee/spec/features/users/login_spec.rb          |   4 +-
 ee/spec/helpers/search_helper_spec.rb         |   2 +-
 .../user_settings/menus/profile_menu.rb       |   4 +-
 public/robots.txt                             |   1 +
 .../oauth/applications_controller_spec.rb     |   2 +-
 spec/controllers/profiles_controller_spec.rb  | 134 ----------------
 .../user_settings/profiles_controller_spec.rb | 145 ++++++++++++++++++
 spec/features/admin/admin_appearance_spec.rb  |   2 +-
 spec/features/admin/admin_mode_spec.rb        |   4 +-
 .../features/file_uploads/user_avatar_spec.rb |   4 +-
 .../profiles/user_edit_profile_spec.rb        |   6 +-
 .../profiles/user_search_settings_spec.rb     |   2 +-
 .../profiles/user_visits_profile_spec.rb      |   2 +-
 .../registrations/oauth_registration_spec.rb  |   2 +-
 spec/features/security/profile_access_spec.rb |   4 +-
 .../user_uploads_avatar_to_profile_spec.rb    |   4 +-
 .../user_sees_active_nav_items_spec.rb        |   2 +-
 spec/features/user_settings/password_spec.rb  |   2 +-
 spec/features/users/login_spec.rb             |   2 +-
 .../password_prompt_modal_spec.js             |   4 +-
 .../components/global_search/mock_data.js     |   2 +-
 .../vue_merge_request_widget/mock_data.js     |   2 +-
 spec/helpers/sidebars_helper_spec.rb          |   4 +-
 spec/helpers/tree_helper_spec.rb              |   2 +-
 .../user_settings/menus/profile_menu_spec.rb  |   4 +-
 spec/presenters/user_presenter_spec.rb        |   5 +-
 spec/requests/legacy_routes_spec.rb           |  15 ++
 spec/requests/robots_txt_spec.rb              |   1 +
 spec/routing/routing_spec.rb                  |  12 +-
 .../profiles/show.html.haml_spec.rb           |   4 +-
 70 files changed, 352 insertions(+), 248 deletions(-)
 rename app/assets/javascripts/pages/{ => user_settings}/profiles/show/index.js (56%)
 rename app/assets/javascripts/pages/{profiles => user_settings/profiles/show}/init_timezone_dropdown.js (100%)
 rename app/assets/javascripts/{pages/profiles => profile}/password_prompt/constants.js (100%)
 rename app/assets/javascripts/{pages/profiles => profile}/password_prompt/index.js (100%)
 rename app/assets/javascripts/{pages/profiles => profile}/password_prompt/password_prompt_modal.vue (100%)
 create mode 100644 app/controllers/user_settings/profiles_controller.rb
 rename app/views/{ => user_settings}/profiles/_email_settings.html.haml (100%)
 rename app/views/{ => user_settings}/profiles/_name.html.haml (100%)
 rename app/views/{ => user_settings}/profiles/show.html.haml (96%)
 delete mode 100644 ee/app/views/profiles/_email_settings.html.haml
 create mode 100644 ee/app/views/user_settings/profiles/_email_settings.html.haml
 rename ee/app/views/{ => user_settings}/profiles/_name.html.haml (100%)
 rename ee/spec/controllers/{ => user_settings}/profiles_controller_spec.rb (89%)
 create mode 100644 spec/controllers/user_settings/profiles_controller_spec.rb
 rename spec/frontend/{pages/profiles => profile}/password_prompt/password_prompt_modal_spec.js (94%)
 rename spec/views/{ => user_settings}/profiles/show.html.haml_spec.rb (89%)

diff --git a/.rubocop_todo/style/hash_as_last_array_item.yml b/.rubocop_todo/style/hash_as_last_array_item.yml
index 21399692bbe14..d9d3dc7f31abb 100644
--- a/.rubocop_todo/style/hash_as_last_array_item.yml
+++ b/.rubocop_todo/style/hash_as_last_array_item.yml
@@ -7,6 +7,7 @@ Style/HashAsLastArrayItem:
     - 'app/controllers/admin/users_controller.rb'
     - 'app/controllers/concerns/issuable_actions.rb'
     - 'app/controllers/concerns/issuable_collections.rb'
+    - 'app/controllers/user_settings/profiles_controller.rb'
     - 'app/controllers/profiles_controller.rb'
     - 'app/controllers/projects/feature_flags_controller.rb'
     - 'app/controllers/projects/merge_requests/application_controller.rb'
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
index b576aab9291f5..363d3a3fd4edb 100644
--- a/app/assets/javascripts/pages/profiles/index.js
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -2,9 +2,6 @@ import $ from 'jquery';
 import '~/profile/gl_crop';
 import Profile from '~/profile/profile';
 import initSearchSettings from '~/search_settings';
-import LengthValidator from '~/validators/length_validator';
-import initPasswordPrompt from './password_prompt';
-import { initTimezoneDropdown } from './init_timezone_dropdown';
 
 // eslint-disable-next-line func-names
 $(document).on('input.ssh_key', '#key_key', function () {
@@ -19,9 +16,5 @@ $(document).on('input.ssh_key', '#key_key', function () {
   }
 });
 
-new Profile(); // eslint-disable-line no-new
-new LengthValidator(); // eslint-disable-line no-new
-
 initSearchSettings();
-initPasswordPrompt();
-initTimezoneDropdown();
+new Profile(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/profiles/show/index.js b/app/assets/javascripts/pages/user_settings/profiles/show/index.js
similarity index 56%
rename from app/assets/javascripts/pages/profiles/show/index.js
rename to app/assets/javascripts/pages/user_settings/profiles/show/index.js
index 69457adf94e9f..a6b4ddd1216b7 100644
--- a/app/assets/javascripts/pages/profiles/show/index.js
+++ b/app/assets/javascripts/pages/user_settings/profiles/show/index.js
@@ -1,12 +1,24 @@
 import emojiRegex from 'emoji-regex';
 import { __ } from '~/locale';
-import { initSetStatusForm } from '~/profile/profile';
+import Profile, { initSetStatusForm } from '~/profile/profile';
 import { initProfileEdit } from '~/profile/edit';
+import '~/profile/gl_crop';
+import initSearchSettings from '~/search_settings';
+import LengthValidator from '~/validators/length_validator';
+import initPasswordPrompt from '~/profile/password_prompt';
+import { initTimezoneDropdown } from './init_timezone_dropdown';
 
 initSetStatusForm();
 // It will do nothing for now when the feature flag is turned off
 initProfileEdit();
 
+new Profile(); // eslint-disable-line no-new
+new LengthValidator(); // eslint-disable-line no-new
+
+initSearchSettings();
+initPasswordPrompt();
+initTimezoneDropdown();
+
 const userNameInput = document.getElementById('user_name');
 if (userNameInput) {
   userNameInput.addEventListener('input', () => {
diff --git a/app/assets/javascripts/pages/profiles/init_timezone_dropdown.js b/app/assets/javascripts/pages/user_settings/profiles/show/init_timezone_dropdown.js
similarity index 100%
rename from app/assets/javascripts/pages/profiles/init_timezone_dropdown.js
rename to app/assets/javascripts/pages/user_settings/profiles/show/init_timezone_dropdown.js
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/constants.js b/app/assets/javascripts/profile/password_prompt/constants.js
similarity index 100%
rename from app/assets/javascripts/pages/profiles/password_prompt/constants.js
rename to app/assets/javascripts/profile/password_prompt/constants.js
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/index.js b/app/assets/javascripts/profile/password_prompt/index.js
similarity index 100%
rename from app/assets/javascripts/pages/profiles/password_prompt/index.js
rename to app/assets/javascripts/profile/password_prompt/index.js
diff --git a/app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue b/app/assets/javascripts/profile/password_prompt/password_prompt_modal.vue
similarity index 100%
rename from app/assets/javascripts/pages/profiles/password_prompt/password_prompt_modal.vue
rename to app/assets/javascripts/profile/password_prompt/password_prompt_modal.vue
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 2ed0b860906ef..f6e5d1a7f8ad3 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -353,7 +353,7 @@ def hexdigest(string)
 
   def require_email
     if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
-      redirect_to profile_path, notice: _('Please complete your profile with email address')
+      redirect_to user_settings_profile_path, notice: _('Please complete your profile with email address')
     end
   end
 
diff --git a/app/controllers/concerns/confirm_email_warning.rb b/app/controllers/concerns/confirm_email_warning.rb
index c55911eed48ca..832e441ab3e71 100644
--- a/app/controllers/concerns/confirm_email_warning.rb
+++ b/app/controllers/concerns/confirm_email_warning.rb
@@ -22,7 +22,7 @@ def set_confirm_warning
       confirm_warning_message,
       email: email_to_display,
       resend_link: view_context.link_to(_('Resend it'), user_confirmation_path(user: { email: email }), method: :post),
-      update_link: view_context.link_to(_('Update it'), profile_path)
+      update_link: view_context.link_to(_('Update it'), user_settings_profile_path)
     ).html_safe
   end
 
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index 2d5421f9f746b..f08c5d0dce92b 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -56,7 +56,7 @@ def renew
   def verify_user_oauth_applications_enabled
     return if Gitlab::CurrentSettings.user_oauth_applications?
 
-    redirect_to profile_path
+    redirect_to user_settings_profile_path
   end
 
   def set_index_vars
diff --git a/app/controllers/profiles/avatars_controller.rb b/app/controllers/profiles/avatars_controller.rb
index 829a87b7d0a1d..e3fc3ee42beed 100644
--- a/app/controllers/profiles/avatars_controller.rb
+++ b/app/controllers/profiles/avatars_controller.rb
@@ -8,6 +8,6 @@ def destroy
 
     Users::UpdateService.new(current_user, user: @user).execute(&:remove_avatar!)
 
-    redirect_to profile_path, status: :found
+    redirect_to user_settings_profile_path, status: :found
   end
 end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 39a070b6405d4..7315be9a4adfb 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -9,32 +9,10 @@ class ProfilesController < Profiles::ApplicationController
   before_action only: :update_username do
     check_rate_limit!(:profile_update_username, scope: current_user)
   end
-  skip_before_action :require_email, only: [:show, :update]
 
-  feature_category :user_profile, [:show, :update, :reset_incoming_email_token, :reset_feed_token,
+  feature_category :user_profile, [:reset_incoming_email_token, :reset_feed_token,
                             :reset_static_object_token, :update_username]
 
-  urgency :low, [:show, :update]
-
-  def show
-  end
-
-  def update
-    respond_to do |format|
-      result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute(check_password: true)
-
-      if result[:status] == :success
-        message = s_("Profiles|Profile was successfully updated")
-
-        format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
-        format.json { render json: { message: message } }
-      else
-        format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: result[:message] }) }
-        format.json { render json: result }
-      end
-    end
-  end
-
   def reset_incoming_email_token
     Users::UpdateService.new(current_user, user: @user).execute! do |user|
       user.reset_incoming_email_token!
@@ -71,12 +49,12 @@ def update_username
       if result[:status] == :success
         message = s_("Profiles|Username successfully changed")
 
-        format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+        format.html { redirect_back_or_default(default: user_settings_profile_path, options: { notice: message }) }
         format.json { render json: { message: message }, status: :ok }
       else
         message = s_("Profiles|Username change failed - %{message}") % { message: result[:message] }
 
-        format.html { redirect_back_or_default(default: { action: 'show' }, options: { alert: message }) }
+        format.html { redirect_back_or_default(default: user_settings_profile_path, options: { alert: message }) }
         format.json { render json: { message: message }, status: :unprocessable_entity }
       end
     end
diff --git a/app/controllers/user_settings/profiles_controller.rb b/app/controllers/user_settings/profiles_controller.rb
new file mode 100644
index 0000000000000..17ebe58716482
--- /dev/null
+++ b/app/controllers/user_settings/profiles_controller.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module UserSettings
+  class ProfilesController < ApplicationController
+    include ActionView::Helpers::SanitizeHelper
+    include Gitlab::Tracking
+
+    before_action :user
+    skip_before_action :require_email, only: [:show, :update]
+    feature_category :user_profile, [:show, :update]
+
+    urgency :low, [:show, :update]
+
+    def show; end
+
+    def update
+      respond_to do |format|
+        result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute(check_password: true)
+
+        if result[:status] == :success
+          message = s_("Profiles|Profile was successfully updated")
+
+          format.html { redirect_back_or_default(default: { action: 'show' }, options: { notice: message }) }
+          format.json { render json: { message: message } }
+        else
+          format.html do
+            redirect_back_or_default(default: { action: 'show' }, options: { alert: result[:message] })
+          end
+          format.json { render json: result }
+        end
+      end
+    end
+
+    private
+
+    def user
+      @user = current_user
+    end
+
+    def user_params_attributes
+      [
+        :avatar,
+        :bio,
+        :discord,
+        :email,
+        :role,
+        :gitpod_enabled,
+        :hide_no_password,
+        :hide_no_ssh_key,
+        :hide_project_limit,
+        :linkedin,
+        :location,
+        :mastodon,
+        :name,
+        :public_email,
+        :commit_email,
+        :skype,
+        :twitter,
+        :username,
+        :website_url,
+        :organization,
+        :private_profile,
+        :include_private_contributions,
+        :achievements_enabled,
+        :timezone,
+        :job_title,
+        :pronouns,
+        :pronunciation,
+        :validation_password,
+        status: [:emoji, :message, :availability, :clear_status_after]
+      ]
+    end
+
+    def user_params
+      @user_params ||= params.require(:user).permit(user_params_attributes)
+    end
+  end
+end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index c115e4c594ab2..85ce4a7dcfbdb 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -72,7 +72,7 @@ def prevent_delete_account?
 
   def user_profile_data(user)
     {
-      profile_path: profile_path,
+      profile_path: user_settings_profile_path,
       profile_avatar_path: profile_avatar_path,
       avatar_url: avatar_icon_for_user(user, current_user: current_user),
       has_avatar: user.avatar?.to_s,
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 2ee2088712956..8d1b9aa3dea19 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -253,7 +253,7 @@ def search_scope
   # Autocomplete results for various settings pages
   def default_autocomplete
     [
-      { category: "Settings", label: _("User settings"),    url: profile_path },
+      { category: "Settings", label: _("User settings"),    url: user_settings_profile_path },
       { category: "Settings", label: _("SSH Keys"),         url: profile_keys_path },
       { category: "Settings", label: _("Dashboard"),        url: root_path }
     ]
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 92a4f32dfda3f..0ffdf46238cc4 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -85,7 +85,7 @@ def super_sidebar_logged_in_context(user, group:, project:, panel:, panel_type:)
       status: user_status_menu_data(user),
       settings: {
         has_settings: current_user_menu?(:settings),
-        profile_path: profile_path,
+        profile_path: user_settings_profile_path,
         profile_preferences_path: profile_preferences_path
       },
       user_counts: {
@@ -359,7 +359,7 @@ def context_switcher_links
     links = [
       ({ title: s_('Navigation|Your work'), link: root_path, icon: 'work' } if current_user),
       { title: s_('Navigation|Explore'), link: explore_root_path, icon: 'compass' },
-      ({ title: s_('Navigation|Profile'), link: profile_path, icon: 'profile' } if current_user),
+      ({ title: s_('Navigation|Profile'), link: user_settings_profile_path, icon: 'profile' } if current_user),
       ({ title: s_('Navigation|Preferences'), link: profile_preferences_path, icon: 'preferences' } if current_user)
     ]
 
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 880fb8aa9d825..8c9102e6b7a7d 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -192,7 +192,7 @@ def web_ide_button_data(options = {})
 
       gitpod_url: gitpod_url,
       user_preferences_gitpod_path: profile_preferences_path(anchor: 'user_gitpod_enabled'),
-      user_profile_enable_gitpod_path: profile_path(user: { gitpod_enabled: true })
+      user_profile_enable_gitpod_path: user_settings_profile_path(user: { gitpod_enabled: true })
     }
   end
 
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index da087ce685830..6ff2964792b20 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -16,7 +16,7 @@ def preferences_gitpod_path
   end
 
   def profile_enable_gitpod_path
-    profile_path(user: { gitpod_enabled: true }) if application_gitpod_enabled?
+    user_settings_profile_path(user: { gitpod_enabled: true }) if application_gitpod_enabled?
   end
 
   delegator_override :saved_replies
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index bb3c02cabdb2c..96effccbfe4e9 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,5 +1,5 @@
 - page_title    _("User Settings")
-- header_title  _("User Settings"), profile_path unless header_title
+- header_title  _("User Settings"), user_settings_profile_path unless header_title
 - sidebar       "dashboard"
 - nav           "profile"
 
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 3f18a7bbda62e..d8bc4cd02a506 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,5 +1,5 @@
 - page_title _('Emails')
-- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{profile_path}' class='gl-text-blue-500!'>".html_safe, closingTag: '</a>'.html_safe}
+- profile_message = _('Used for avatar detection. You can change it in your %{openingTag}profile settings%{closingTag}.') % { openingTag: "<a href='#{user_settings_profile_path}' class='gl-text-blue-500!'>".html_safe, closingTag: '</a>'.html_safe}
 - notification_message = _('Used for account notifications if a %{openingTag}group-specific email address%{closingTag} is not set.') % { openingTag: "<a href='#{profile_notifications_path}' class='gl-text-blue-500!'>".html_safe, closingTag: '</a>'.html_safe}
 - public_email_message = _('Your public email will be displayed on your public profile.')
 - commit_email_message = _('Used for web based operations, such as edits and merges.')
diff --git a/app/views/profiles/slacks/edit.html.haml b/app/views/profiles/slacks/edit.html.haml
index 202747356507b..a4c3351b27cec 100644
--- a/app/views/profiles/slacks/edit.html.haml
+++ b/app/views/profiles/slacks/edit.html.haml
@@ -1,4 +1,4 @@
-- add_to_breadcrumbs _('Profile'), profile_path
+- add_to_breadcrumbs _('Profile'), user_settings_profile_path
 - @hide_top_links = true
 - @content_class = 'limit-container-width'
 - page_title s_('SlackIntegration|GitLab for Slack')
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 721446eb01744..e2a1eb4bd9fb4 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -21,7 +21,7 @@
     window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}';
     window.gl.mrWidgetData.can_view_false_positive = '#{@merge_request.project.licensed_feature_available?(:sast_fp_reduction).to_s}';
     window.gl.mrWidgetData.user_preferences_gitpod_path = '#{profile_preferences_path(anchor: 'user_gitpod_enabled')}';
-    window.gl.mrWidgetData.user_profile_enable_gitpod_path = '#{profile_path(user: { gitpod_enabled: true })}';
+    window.gl.mrWidgetData.user_profile_enable_gitpod_path = '#{user_settings_profile_path(user: { gitpod_enabled: true })}';
     window.gl.mrWidgetData.saml_approval_path = window.gl.mrWidgetData.saml_approval_path
 
 %h2#merge-request-widgets-heading.gl-sr-only
diff --git a/app/views/shared/_no_password.html.haml b/app/views/shared/_no_password.html.haml
index 1f6f41187fc96..a99e520584c3f 100644
--- a/app/views/shared/_no_password.html.haml
+++ b/app/views/shared/_no_password.html.haml
@@ -6,4 +6,4 @@
       = no_password_message
     - c.with_actions do
       = link_button_to _('Remind later'), '#', class: 'js-hide-no-password-message gl-alert-action', variant: :confirm
-      = link_button_to _("Don't show again"), profile_path(user: { hide_no_password: true }), method: :put, role: 'button', class: 'gl-alert-action'
+      = link_button_to _("Don't show again"), user_settings_profile_path(user: { hide_no_password: true }), method: :put, role: 'button', class: 'gl-alert-action'
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index a3f24da5d7cf9..e264044ea7fc0 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -6,4 +6,4 @@
       = s_("MissingSSHKeyWarningLink|You can't push or pull repositories using SSH until you add an SSH key to your profile.")
     - c.with_actions do
       = link_button_to s_('MissingSSHKeyWarningLink|Add SSH key'), profile_keys_path, class: 'gl-alert-action', variant: :confirm
-      = link_button_to s_("MissingSSHKeyWarningLink|Don't show again"), profile_path(user: { hide_no_ssh_key: true }), method: :put, role: 'button', class: 'gl-alert-action'
+      = link_button_to s_("MissingSSHKeyWarningLink|Don't show again"), user_settings_profile_path(user: { hide_no_ssh_key: true }), method: :put, role: 'button', class: 'gl-alert-action'
diff --git a/app/views/shared/_project_limit.html.haml b/app/views/shared/_project_limit.html.haml
index 914c20fb7b06c..aedcb57cb9c1c 100644
--- a/app/views/shared/_project_limit.html.haml
+++ b/app/views/shared/_project_limit.html.haml
@@ -6,4 +6,4 @@
       = _("You cannot create new projects in your personal namespace because you have reached your personal project limit.")
     - c.with_actions do
       = link_button_to _('Remind later'), '#', class: 'alert-link hide-project-limit-message', variant: :confirm
-      = link_button_to _("Don't show again"), profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link gl-ml-3'
+      = link_button_to _("Don't show again"), user_settings_profile_path(user: {hide_project_limit: true}), method: :put, class: 'alert-link gl-ml-3'
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/user_settings/profiles/_email_settings.html.haml
similarity index 100%
rename from app/views/profiles/_email_settings.html.haml
rename to app/views/user_settings/profiles/_email_settings.html.haml
diff --git a/app/views/profiles/_name.html.haml b/app/views/user_settings/profiles/_name.html.haml
similarity index 100%
rename from app/views/profiles/_name.html.haml
rename to app/views/user_settings/profiles/_name.html.haml
diff --git a/app/views/profiles/show.html.haml b/app/views/user_settings/profiles/show.html.haml
similarity index 96%
rename from app/views/profiles/show.html.haml
rename to app/views/user_settings/profiles/show.html.haml
index 6d8755718ad37..c0cf6c5ee3740 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/user_settings/profiles/show.html.haml
@@ -7,7 +7,7 @@
 - if Feature.enabled?(:edit_user_profile_vue, current_user)
   .js-user-profile{ data: user_profile_data(@user) }
 - else
-  = gitlab_ui_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
+  = gitlab_ui_form_for @user, url: user_settings_profile_path, method: :put, html: { multipart: true, class: 'edit-user js-edit-user js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
     .settings-section.js-search-settings-section
       .settings-sticky-header
         .settings-sticky-header-inner
@@ -78,7 +78,7 @@
         - if current_user.ldap_user?
           = s_("Profiles|Some options are unavailable for LDAP accounts")
       .form-group.gl-form-group.rspec-full-name.gl-max-w-80
-        = render 'profiles/name', form: f, user: @user
+        = render 'user_settings/profiles/name', form: f, user: @user
       .form-group.gl-form-group.gl-md-form-input-lg
         = f.label :id, s_('Profiles|User ID')
         = f.text_field :id, class: 'gl-form-input form-control', readonly: true
@@ -93,7 +93,7 @@
         %small.form-text.text-gl-muted
           = s_("Profiles|Enter how your name is pronounced to help people address you correctly.")
       = render_if_exists 'profiles/extra_settings', form: f
-      = render_if_exists 'profiles/email_settings', form: f
+      = render_if_exists 'user_settings/profiles/email_settings', form: f
       .form-group.gl-form-group
         = f.label :skype
         = f.text_field :skype, class: 'gl-form-input form-control gl-md-form-input-lg', placeholder: s_("Profiles|username")
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 3aee73b0b961e..0f6d08dee8b9d 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -19,7 +19,7 @@
         = render 'users/follow_user'
         -# The following edit button is mutually exclusive to the follow user button, they won't be shown together
         - if @user == current_user
-          = render Pajamas::ButtonComponent.new(href: profile_path,
+          = render Pajamas::ButtonComponent.new(href: user_settings_profile_path,
             button_options: { class: 'gl-flex-grow-1', title: s_('UserProfile|Edit profile') }) do
             = s_("UserProfile|Edit profile")
         = render 'users/view_gpg_keys'
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
index eed42105db1a0..48a5e4f423891 100644
--- a/config/routes/profile.rb
+++ b/config/routes/profile.rb
@@ -3,7 +3,7 @@
 # for secondary email confirmations - uses the same confirmation controller as :users
 devise_for :emails, path: 'profile/emails', controllers: { confirmations: :confirmations }
 
-resource :profile, only: [:show, :update] do
+resource :profile, only: [] do
   member do
     get :audit_log, to: redirect('-/user_settings/authentication_log')
     get :applications, to: redirect('-/user_settings/applications')
diff --git a/config/routes/user_settings.rb b/config/routes/user_settings.rb
index e815e29d3233f..62f0685b63721 100644
--- a/config/routes/user_settings.rb
+++ b/config/routes/user_settings.rb
@@ -6,6 +6,7 @@
     get :applications, to: '/oauth/applications#index'
   end
   resources :active_sessions, only: [:index, :destroy]
+  resource :profile, only: [:show, :update]
   resource :password, only: [:new, :create, :edit, :update] do
     member do
       put :reset
@@ -28,6 +29,10 @@
     end
   end
   member do
+    get :show, to: redirect(path: '-/user_settings/profile')
+    put :update, controller: 'user_settings/profiles'
+    patch :update, controller: 'user_settings/profiles'
+
     get :active_sessions, to: redirect(path: '-/user_settings/active_sessions')
     get :personal_access_tokens, to: redirect(path: '-/user_settings/personal_access_tokens')
   end
diff --git a/doc/user/project/integrations/webhook_events.md b/doc/user/project/integrations/webhook_events.md
index f3c7d002e7423..5253fd9d64b94 100644
--- a/doc/user/project/integrations/webhook_events.md
+++ b/doc/user/project/integrations/webhook_events.md
@@ -40,7 +40,7 @@ Event type                                   | Trigger
 
 NOTE:
 If an author has no public email listed in their
-[GitLab profile](https://gitlab.com/-/profile), the `email` attribute in the
+[GitLab profile](https://gitlab.com/-/user_settings/profile), the `email` attribute in the
 webhook payload displays a value of `[REDACTED]`.
 
 ## Push events
diff --git a/ee/app/assets/javascripts/vue_shared/purchase_flow/constants.js b/ee/app/assets/javascripts/vue_shared/purchase_flow/constants.js
index 4c57623634150..73f43fb571312 100644
--- a/ee/app/assets/javascripts/vue_shared/purchase_flow/constants.js
+++ b/ee/app/assets/javascripts/vue_shared/purchase_flow/constants.js
@@ -9,7 +9,7 @@ export const GENERAL_ERROR_MESSAGE = s__(
 export const licensingAndRenewalsProblemsLink =
   'https://support.gitlab.com/hc/en-us/requests/new?ticket_form_id=360000071293';
 export const salesLink = `${PROMO_URL}/sales/`;
-export const userProfileLink = `https://${DOMAIN}/-/profile`;
+export const userProfileLink = `https://${DOMAIN}/-/user_settings/profile`;
 export const linkCustomersPortalHelpLink = helpPagePath('subscriptions/customers_portal', {
   anchor: '#change-the-linked-account',
 });
diff --git a/ee/app/views/profiles/_email_settings.html.haml b/ee/app/views/profiles/_email_settings.html.haml
deleted file mode 100644
index 2873d28c0787f..0000000000000
--- a/ee/app/views/profiles/_email_settings.html.haml
+++ /dev/null
@@ -1,3 +0,0 @@
-- group_managed_account = @user.group_managed_account?
-
-= render_ce 'profiles/email_settings', form: form, email_change_disabled: group_managed_account
diff --git a/ee/app/views/user_settings/profiles/_email_settings.html.haml b/ee/app/views/user_settings/profiles/_email_settings.html.haml
new file mode 100644
index 0000000000000..3905b3b29b288
--- /dev/null
+++ b/ee/app/views/user_settings/profiles/_email_settings.html.haml
@@ -0,0 +1,3 @@
+- group_managed_account = @user.group_managed_account?
+
+= render_ce 'user_settings/profiles/email_settings', form: form, email_change_disabled: group_managed_account
diff --git a/ee/app/views/profiles/_name.html.haml b/ee/app/views/user_settings/profiles/_name.html.haml
similarity index 100%
rename from ee/app/views/profiles/_name.html.haml
rename to ee/app/views/user_settings/profiles/_name.html.haml
diff --git a/ee/spec/controllers/profiles_controller_spec.rb b/ee/spec/controllers/user_settings/profiles_controller_spec.rb
similarity index 89%
rename from ee/spec/controllers/profiles_controller_spec.rb
rename to ee/spec/controllers/user_settings/profiles_controller_spec.rb
index 0497fca055fba..4d524bdafa628 100644
--- a/ee/spec/controllers/profiles_controller_spec.rb
+++ b/ee/spec/controllers/user_settings/profiles_controller_spec.rb
@@ -2,13 +2,13 @@
 
 require('spec_helper')
 
-RSpec.describe ProfilesController, :request_store do
+RSpec.describe UserSettings::ProfilesController, :request_store, feature_category: :user_profile do
   let_it_be(:user) { create(:user) }
   let_it_be(:admin) { create(:admin) }
 
   describe 'PUT update', feature_category: :user_profile do
-    context 'updating name' do
-      subject { put :update, params: { user: { name: 'New Name' } } }
+    context 'on updating name' do
+      subject(:perform_update) { put :update, params: { user: { name: 'New Name' } } }
 
       shared_examples_for 'a user can update their name' do
         before do
@@ -16,7 +16,7 @@
         end
 
         it 'updates their name' do
-          subject
+          perform_update
 
           expect(response).to have_gitlab_http_status(:found)
           expect(current_user.reload.name).to eq('New Name')
@@ -53,7 +53,7 @@
             end
 
             it 'does not update their name' do
-              subject
+              perform_update
 
               expect(response).to have_gitlab_http_status(:found)
               expect(user.reload.name).not_to eq('New Name')
@@ -84,8 +84,8 @@
     end
   end
 
-  context 'updating public profile to private' do
-    subject { put :update, params: { user: { private_profile: true } } }
+  context 'on updating public profile to private' do
+    subject(:perform_update) { put :update, params: { user: { private_profile: true } } }
 
     shared_examples_for 'a user can make their profile private' do
       before do
@@ -93,7 +93,7 @@
       end
 
       it 'updates their profile to private' do
-        subject
+        perform_update
 
         expect(response).to have_gitlab_http_status(:found)
         expect(current_user.reload.private_profile).to be true
@@ -106,7 +106,7 @@
       end
 
       it 'does not update their profile to private' do
-        subject
+        perform_update
         expect(current_user.reload.private_profile).not_to be true
       end
     end
diff --git a/ee/spec/features/profiles/usage_quotas_spec.rb b/ee/spec/features/profiles/usage_quotas_spec.rb
index 2d3bc8ef0c636..c0cebdba88064 100644
--- a/ee/spec/features/profiles/usage_quotas_spec.rb
+++ b/ee/spec/features/profiles/usage_quotas_spec.rb
@@ -18,7 +18,7 @@
   end
 
   it 'is linked within the profile page' do
-    visit profile_path
+    visit user_settings_profile_path
 
     within_testid('super-sidebar') do
       expect(page).to have_selector(:link_or_button, 'Usage Quotas')
diff --git a/ee/spec/features/profiles/user_visits_profile_spec.rb b/ee/spec/features/profiles/user_visits_profile_spec.rb
index a4a5e54f21dfe..df2b416b7eae0 100644
--- a/ee/spec/features/profiles/user_visits_profile_spec.rb
+++ b/ee/spec/features/profiles/user_visits_profile_spec.rb
@@ -30,7 +30,7 @@
       end
 
       it 'displays the banner in the profile page' do
-        visit(profile_path)
+        visit(user_settings_profile_path)
         expect(page).to have_text storage_banner_text
       end
     end
@@ -41,7 +41,7 @@
       end
 
       it 'does not display the banner in the group page' do
-        visit(profile_path)
+        visit(user_settings_profile_path)
         expect(page).not_to have_text storage_banner_text
       end
     end
diff --git a/ee/spec/features/security/profile_access_spec.rb b/ee/spec/features/security/profile_access_spec.rb
index f2ac8930c1afb..e66c661c2db7c 100644
--- a/ee/spec/features/security/profile_access_spec.rb
+++ b/ee/spec/features/security/profile_access_spec.rb
@@ -11,8 +11,8 @@
     it { is_expected.to be_allowed_for :auditor }
   end
 
-  describe "GET /-/profile" do
-    subject { profile_path }
+  describe "GET /-/user_settings/profile" do
+    subject { user_settings_profile_path }
 
     it { is_expected.to be_allowed_for :auditor }
   end
diff --git a/ee/spec/features/users/login_spec.rb b/ee/spec/features/users/login_spec.rb
index 5325097bf3ae0..a3d3aec560c49 100644
--- a/ee/spec/features/users/login_spec.rb
+++ b/ee/spec/features/users/login_spec.rb
@@ -112,7 +112,7 @@
               # Loging using smartcard
               visit verify_certificate_smartcard_path(client_certificate: encrypted_openssl_certificate)
 
-              visit profile_path
+              visit user_settings_profile_path
 
               expect(page).not_to have_content(_('Enter verification code'))
             end
@@ -122,7 +122,7 @@
             it 'asks for Two-Factor Authentication' do
               sign_in(user)
 
-              visit profile_path
+              visit user_settings_profile_path
 
               expect(page).to have_content(_('Enter verification code'))
             end
diff --git a/ee/spec/helpers/search_helper_spec.rb b/ee/spec/helpers/search_helper_spec.rb
index ed87c0f5bc82c..2fa1a6abe845f 100644
--- a/ee/spec/helpers/search_helper_spec.rb
+++ b/ee/spec/helpers/search_helper_spec.rb
@@ -191,7 +191,7 @@
           expect(results.first).to include({
             category: 'Settings',
             label: 'User settings',
-            url: Gitlab::Routing.url_helpers.profile_path
+            url: Gitlab::Routing.url_helpers.user_settings_profile_path
           })
         end
       end
diff --git a/lib/sidebars/user_settings/menus/profile_menu.rb b/lib/sidebars/user_settings/menus/profile_menu.rb
index 73119070586f2..77b95321c226f 100644
--- a/lib/sidebars/user_settings/menus/profile_menu.rb
+++ b/lib/sidebars/user_settings/menus/profile_menu.rb
@@ -8,7 +8,7 @@ class ProfileMenu < ::Sidebars::Menu
 
         override :link
         def link
-          profile_path
+          user_settings_profile_path
         end
 
         override :title
@@ -23,7 +23,7 @@ def sprite_icon
 
         override :active_routes
         def active_routes
-          { path: 'profiles#show' }
+          { path: 'user_settings/profiles#show' }
         end
       end
     end
diff --git a/public/robots.txt b/public/robots.txt
index 6c0513fd6c42b..2e3d9d6cc9657 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -24,6 +24,7 @@ Disallow: /api/v*
 Disallow: /help
 Disallow: /s/
 Disallow: /-/profile
+Disallow: /-/user_settings/profile
 Disallow: /-/ide/
 Disallow: /-/experiment
 # Restrict allowed routes to avoid very ugly search results
diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb
index dcd817861a7ed..5f06ab6137dcb 100644
--- a/spec/controllers/oauth/applications_controller_spec.rb
+++ b/spec/controllers/oauth/applications_controller_spec.rb
@@ -168,7 +168,7 @@
         subject
 
         expect(response).to have_gitlab_http_status(:found)
-        expect(response).to redirect_to(profile_path)
+        expect(response).to redirect_to(user_settings_profile_path)
       end
 
       context 'when redirect_uri is invalid' do
diff --git a/spec/controllers/profiles_controller_spec.rb b/spec/controllers/profiles_controller_spec.rb
index 26144edb67046..20e5b2ffa58a5 100644
--- a/spec/controllers/profiles_controller_spec.rb
+++ b/spec/controllers/profiles_controller_spec.rb
@@ -6,140 +6,6 @@
   let(:password) { User.random_password }
   let(:user) { create(:user, password: password) }
 
-  describe 'POST update' do
-    it 'does not update password' do
-      sign_in(user)
-      new_password = User.random_password
-      expect do
-        post :update, params: { user: { password: new_password, password_confirmation: new_password } }
-      end.not_to change { user.reload.encrypted_password }
-
-      expect(response).to have_gitlab_http_status(:found)
-    end
-  end
-
-  describe 'PUT update' do
-    it 'allows an email update from a user without an external email address' do
-      sign_in(user)
-
-      put :update, params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
-
-      user.reload
-
-      expect(response).to have_gitlab_http_status(:found)
-      expect(user.unconfirmed_email).to eq('john@gmail.com')
-    end
-
-    it "allows an email update without confirmation if existing verified email" do
-      user = create(:user)
-      create(:email, :confirmed, user: user, email: 'john@gmail.com')
-      sign_in(user)
-
-      put :update, params: { user: { email: "john@gmail.com", name: "John" } }
-
-      user.reload
-
-      expect(response).to have_gitlab_http_status(:found)
-      expect(user.unconfirmed_email).to eq nil
-    end
-
-    it 'ignores an email update from a user with an external email address' do
-      stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
-      stub_omniauth_setting(sync_profile_attributes: true)
-
-      ldap_user = create(:omniauth_user)
-      ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true)
-      sign_in(ldap_user)
-
-      put :update, params: { user: { email: "john@gmail.com", name: "John" } }
-
-      ldap_user.reload
-
-      expect(response).to have_gitlab_http_status(:found)
-      expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
-    end
-
-    it 'ignores an email and name update but allows a location update from a user with external email and name, but not external location' do
-      stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
-      stub_omniauth_setting(sync_profile_attributes: true)
-
-      ldap_user = create(:omniauth_user, name: 'Alex')
-      ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true, location_synced: false)
-      sign_in(ldap_user)
-
-      put :update, params: { user: { email: "john@gmail.com", name: "John", location: "City, Country" } }
-
-      ldap_user.reload
-
-      expect(response).to have_gitlab_http_status(:found)
-      expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
-      expect(ldap_user.name).not_to eq('John')
-      expect(ldap_user.location).to eq('City, Country')
-    end
-
-    it 'allows setting a user status', :freeze_time do
-      sign_in(user)
-
-      put :update, params: { user: { status: { message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours' } } }
-
-      expect(user.reload.status.message).to eq('Working hard!')
-      expect(user.reload.status.availability).to eq('busy')
-      expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-
-    it 'allows updating user specified job title' do
-      title = 'Marketing Executive'
-      sign_in(user)
-
-      put :update, params: { user: { job_title: title } }
-
-      expect(user.reload.job_title).to eq(title)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-
-    it 'allows updating user specified pronouns', :aggregate_failures do
-      pronouns = 'they/them'
-      sign_in(user)
-
-      put :update, params: { user: { pronouns: pronouns } }
-
-      expect(user.reload.pronouns).to eq(pronouns)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-
-    it 'allows updating user specified pronunciation', :aggregate_failures do
-      user = create(:user, name: 'Example')
-      pronunciation = 'uhg-zaam-pl'
-      sign_in(user)
-
-      put :update, params: { user: { pronunciation: pronunciation } }
-
-      expect(user.reload.pronunciation).to eq(pronunciation)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-
-    it 'allows updating user specified Discord User ID', :aggregate_failures do
-      discord_user_id = '1234567890123456789'
-      sign_in(user)
-
-      put :update, params: { user: { discord: discord_user_id } }
-
-      expect(user.reload.discord).to eq(discord_user_id)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-
-    it 'allows updating user specified mastodon username', :aggregate_failures do
-      mastodon_username = '@robin@example.com'
-      sign_in(user)
-
-      put :update, params: { user: { mastodon: mastodon_username } }
-
-      expect(user.reload.mastodon).to eq(mastodon_username)
-      expect(response).to have_gitlab_http_status(:found)
-    end
-  end
-
   describe 'PUT update_username' do
     let(:namespace) { user.namespace }
     let(:gitlab_shell) { Gitlab::Shell.new }
diff --git a/spec/controllers/user_settings/profiles_controller_spec.rb b/spec/controllers/user_settings/profiles_controller_spec.rb
new file mode 100644
index 0000000000000..706612296acbf
--- /dev/null
+++ b/spec/controllers/user_settings/profiles_controller_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+require('spec_helper')
+
+RSpec.describe UserSettings::ProfilesController, :request_store, feature_category: :user_profile do
+  let(:password) { User.random_password }
+  let(:user) { create(:user, password: password) }
+
+  describe 'POST update' do
+    it 'does not update password' do
+      sign_in(user)
+      new_password = User.random_password
+      expect do
+        post :update, params: { user: { password: new_password, password_confirmation: new_password } }
+      end.not_to change { user.reload.encrypted_password }
+
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows an email update from a user without an external email address' do
+      sign_in(user)
+
+      put :update, params: { user: { email: "john@gmail.com", name: "John", validation_password: password } }
+
+      user.reload
+
+      expect(response).to have_gitlab_http_status(:found)
+      expect(user.unconfirmed_email).to eq('john@gmail.com')
+    end
+
+    it "allows an email update without confirmation if existing verified email" do
+      user = create(:user)
+      create(:email, :confirmed, user: user, email: 'john@gmail.com')
+      sign_in(user)
+
+      put :update, params: { user: { email: "john@gmail.com", name: "John" } }
+
+      user.reload
+
+      expect(response).to have_gitlab_http_status(:found)
+      expect(user.unconfirmed_email).to eq nil
+    end
+
+    it 'ignores an email update from a user with an external email address' do
+      stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
+      stub_omniauth_setting(sync_profile_attributes: true)
+
+      ldap_user = create(:omniauth_user)
+      ldap_user.create_user_synced_attributes_metadata(provider: 'ldap', name_synced: true, email_synced: true)
+      sign_in(ldap_user)
+
+      put :update, params: { user: { email: "john@gmail.com", name: "John" } }
+
+      ldap_user.reload
+
+      expect(response).to have_gitlab_http_status(:found)
+      expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
+    end
+
+    it 'ignores an email and name update but allows a location update from a user with external email and name,' \
+       'but not external location' do
+      stub_omniauth_setting(sync_profile_from_provider: ['ldap'])
+      stub_omniauth_setting(sync_profile_attributes: true)
+
+      ldap_user = create(:omniauth_user, name: 'Alex')
+      ldap_user.create_user_synced_attributes_metadata(
+        provider: 'ldap', name_synced: true, email_synced: true, location_synced: false
+      )
+      sign_in(ldap_user)
+
+      put :update, params: { user: { email: "john@gmail.com", name: "John", location: "City, Country" } }
+
+      ldap_user.reload
+
+      expect(response).to have_gitlab_http_status(:found)
+      expect(ldap_user.unconfirmed_email).not_to eq('john@gmail.com')
+      expect(ldap_user.name).not_to eq('John')
+      expect(ldap_user.location).to eq('City, Country')
+    end
+
+    it 'allows setting a user status', :freeze_time do
+      sign_in(user)
+
+      put :update, params: { user: { status: {
+        message: 'Working hard!', availability: 'busy', clear_status_after: '8_hours'
+      } } }
+
+      expect(user.reload.status.message).to eq('Working hard!')
+      expect(user.reload.status.availability).to eq('busy')
+      expect(user.reload.status.clear_status_after).to eq(8.hours.from_now)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows updating user specified job title' do
+      title = 'Marketing Executive'
+      sign_in(user)
+
+      put :update, params: { user: { job_title: title } }
+
+      expect(user.reload.job_title).to eq(title)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows updating user specified pronouns', :aggregate_failures do
+      pronouns = 'they/them'
+      sign_in(user)
+
+      put :update, params: { user: { pronouns: pronouns } }
+
+      expect(user.reload.pronouns).to eq(pronouns)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows updating user specified pronunciation', :aggregate_failures do
+      user = create(:user, name: 'Example')
+      pronunciation = 'uhg-zaam-pl'
+      sign_in(user)
+
+      put :update, params: { user: { pronunciation: pronunciation } }
+
+      expect(user.reload.pronunciation).to eq(pronunciation)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows updating user specified Discord User ID', :aggregate_failures do
+      discord_user_id = '1234567890123456789'
+      sign_in(user)
+
+      put :update, params: { user: { discord: discord_user_id } }
+
+      expect(user.reload.discord).to eq(discord_user_id)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+
+    it 'allows updating user specified mastodon username', :aggregate_failures do
+      mastodon_username = '@robin@example.com'
+      sign_in(user)
+
+      put :update, params: { user: { mastodon: mastodon_username } }
+
+      expect(user.reload.mastodon).to eq(mastodon_username)
+      expect(response).to have_gitlab_http_status(:found)
+    end
+  end
+end
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index ec63e43d1838d..277a12fb8f117 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -120,7 +120,7 @@
 
     it 'renders guidelines when set' do
       sign_in create(:user)
-      visit profile_path
+      visit user_settings_profile_path
 
       expect(page).to have_content 'Custom profile image guidelines, please 😄!'
     end
diff --git a/spec/features/admin/admin_mode_spec.rb b/spec/features/admin/admin_mode_spec.rb
index b58953989d2dc..cb8d9b9b9954d 100644
--- a/spec/features/admin/admin_mode_spec.rb
+++ b/spec/features/admin/admin_mode_spec.rb
@@ -31,7 +31,7 @@
 
         click_link('Edit profile')
 
-        expect(page).to have_current_path(profile_path)
+        expect(page).to have_current_path(user_settings_profile_path)
       end
 
       it 'is necessary to provide credentials again before opening pages in admin scope' do
@@ -86,7 +86,7 @@
 
         click_link('Edit profile')
 
-        expect(page).to have_current_path(profile_path)
+        expect(page).to have_current_path(user_settings_profile_path)
       end
 
       context 'sidebar' do
diff --git a/spec/features/file_uploads/user_avatar_spec.rb b/spec/features/file_uploads/user_avatar_spec.rb
index 3f7d69afa0b6f..e44d75ed582ff 100644
--- a/spec/features/file_uploads/user_avatar_spec.rb
+++ b/spec/features/file_uploads/user_avatar_spec.rb
@@ -10,7 +10,7 @@
   before do
     stub_feature_flags(edit_user_profile_vue: false)
     sign_in(user)
-    visit(profile_path)
+    visit(user_settings_profile_path)
     attach_file('user_avatar-trigger', file.path, make_visible: true)
     click_button 'Set new profile picture'
   end
@@ -27,7 +27,7 @@
       expect(page).to have_content 'Profile was successfully updated'
       expect(user.reload.avatar.file).to be_present
       expect(user.avatar).to be_instance_of AvatarUploader
-      expect(page).to have_current_path(profile_path, ignore_query: true)
+      expect(page).to have_current_path(user_settings_profile_path, ignore_query: true)
     end
   end
 
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index e60589a161b49..b8fb2c59f31d9 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -10,7 +10,7 @@
   before do
     stub_feature_flags(edit_user_profile_vue: false)
     sign_in(user)
-    visit(profile_path)
+    visit(user_settings_profile_path)
   end
 
   def submit_settings
@@ -257,7 +257,7 @@ def select_emoji(emoji_name)
           expect(page).to have_content user_status.message
         end
 
-        visit(profile_path)
+        visit(user_settings_profile_path)
         click_button s_('SetStatusModal|Clear status')
         submit_settings
 
@@ -282,7 +282,7 @@ def select_emoji(emoji_name)
 
         toggle_busy_status
         submit_settings
-        visit profile_path
+        visit user_settings_profile_path
 
         expect(busy_status.checked?).to eq(true)
       end
diff --git a/spec/features/profiles/user_search_settings_spec.rb b/spec/features/profiles/user_search_settings_spec.rb
index 96fe01cd0c219..af40a532c1390 100644
--- a/spec/features/profiles/user_search_settings_spec.rb
+++ b/spec/features/profiles/user_search_settings_spec.rb
@@ -12,7 +12,7 @@
 
   context 'in profile page' do
     before do
-      visit profile_path
+      visit user_settings_profile_path
     end
 
     it_behaves_like 'can search settings', 'Public avatar', 'Main settings'
diff --git a/spec/features/profiles/user_visits_profile_spec.rb b/spec/features/profiles/user_visits_profile_spec.rb
index 37a19ecadb8a7..7edfb542594ff 100644
--- a/spec/features/profiles/user_visits_profile_spec.rb
+++ b/spec/features/profiles/user_visits_profile_spec.rb
@@ -12,7 +12,7 @@
   end
 
   it 'shows profile info' do
-    visit(profile_path)
+    visit(user_settings_profile_path)
 
     expect(page).to have_content "This information will appear on your profile"
   end
diff --git a/spec/features/registrations/oauth_registration_spec.rb b/spec/features/registrations/oauth_registration_spec.rb
index 369df614083a6..9799823a46567 100644
--- a/spec/features/registrations/oauth_registration_spec.rb
+++ b/spec/features/registrations/oauth_registration_spec.rb
@@ -76,7 +76,7 @@
         it 'redirects to the profile path' do
           register_via(provider, uid, email, additional_info: additional_info)
 
-          expect(page).to have_current_path profile_path
+          expect(page).to have_current_path user_settings_profile_path
           expect(page).to have_content('Please complete your profile with email address')
         end
       end
diff --git a/spec/features/security/profile_access_spec.rb b/spec/features/security/profile_access_spec.rb
index 991ff115d3d63..14b0d6159c6e6 100644
--- a/spec/features/security/profile_access_spec.rb
+++ b/spec/features/security/profile_access_spec.rb
@@ -13,8 +13,8 @@
     it { is_expected.to be_denied_for :visitor }
   end
 
-  describe "GET /-/profile" do
-    subject { profile_path }
+  describe "GET /-/user_settings/profile" do
+    subject { user_settings_profile_path }
 
     it { is_expected.to be_allowed_for :admin }
     it { is_expected.to be_allowed_for :user }
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 5d121d9eeba9c..88d2d1c46bc17 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -23,7 +23,7 @@
         expect(find('.gl-avatar')['src']).to start_with 'blob:'
       end
 
-      visit profile_path
+      visit user_settings_profile_path
 
       expect(page.find('.avatar-image .gl-avatar')['src']).to include(
         "/uploads/-/system/user/avatar/#{user.id}/avatar.png"
@@ -64,6 +64,6 @@
 
   def sign_in_and_visit_profile
     sign_in user
-    visit profile_path
+    visit user_settings_profile_path
   end
 end
diff --git a/spec/features/user_sees_active_nav_items_spec.rb b/spec/features/user_sees_active_nav_items_spec.rb
index 1e6b2b8f189c6..46813938967e4 100644
--- a/spec/features/user_sees_active_nav_items_spec.rb
+++ b/spec/features/user_sees_active_nav_items_spec.rb
@@ -12,7 +12,7 @@
   describe 'profile pages' do
     context 'when visiting profile page' do
       before do
-        visit profile_path
+        visit user_settings_profile_path
       end
 
       it 'renders the side navigation with the correct submenu set as active' do
diff --git a/spec/features/user_settings/password_spec.rb b/spec/features/user_settings/password_spec.rb
index 76e3f85e021d9..6595d513ac2d3 100644
--- a/spec/features/user_settings/password_spec.rb
+++ b/spec/features/user_settings/password_spec.rb
@@ -244,7 +244,7 @@ def fill_passwords(password, confirmation)
       it 'needs change user password' do
         stub_application_setting(require_two_factor_authentication: true)
 
-        visit profile_path
+        visit user_settings_profile_path
 
         expect(page).to have_current_path new_user_settings_password_path, ignore_query: true
       end
diff --git a/spec/features/users/login_spec.rb b/spec/features/users/login_spec.rb
index 1d8a44de7d979..c7ce8619aa479 100644
--- a/spec/features/users/login_spec.rb
+++ b/spec/features/users/login_spec.rb
@@ -1085,7 +1085,7 @@ def sign_in_using_saml!
           expect_to_be_on_terms_page
           click_button 'Accept terms'
 
-          expect(page).to have_current_path(profile_path, ignore_query: true)
+          expect(page).to have_current_path(user_settings_profile_path, ignore_query: true)
 
           fill_in 'Email', with: 'hello@world.com'
 
diff --git a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js b/spec/frontend/profile/password_prompt/password_prompt_modal_spec.js
similarity index 94%
rename from spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
rename to spec/frontend/profile/password_prompt/password_prompt_modal_spec.js
index 18a0098a7156f..cb61700d5e1d3 100644
--- a/spec/frontend/pages/profiles/password_prompt/password_prompt_modal_spec.js
+++ b/spec/frontend/profile/password_prompt/password_prompt_modal_spec.js
@@ -4,8 +4,8 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import {
   I18N_PASSWORD_PROMPT_CANCEL_BUTTON,
   I18N_PASSWORD_PROMPT_CONFIRM_BUTTON,
-} from '~/pages/profiles/password_prompt/constants';
-import PasswordPromptModal from '~/pages/profiles/password_prompt/password_prompt_modal.vue';
+} from '~/profile/password_prompt/constants';
+import PasswordPromptModal from '~/profile/password_prompt/password_prompt_modal.vue';
 
 const createComponent = ({ props }) => {
   return shallowMountExtended(PasswordPromptModal, {
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index 61ddfb6cae1f4..f15cbe43e21fd 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -436,7 +436,7 @@ export const MOCK_GROUPED_AUTOCOMPLETE_OPTIONS_SETTINGS_HELP = [
         html_id: 'autocomplete-Settings-0',
         category: 'Settings',
         label: 'User settings',
-        url: '/-/profile',
+        url: '/-/user_settings/profile',
       },
       {
         html_id: 'autocomplete-Settings-3',
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 34f147307fc27..a2e3fa1ba0734 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -377,7 +377,7 @@ export default {
   show_gitpod_button: true,
   gitpod_url: 'http://gitpod.localhost',
   user_preferences_gitpod_path: '/-/profile/preferences#user_gitpod_enabled',
-  user_profile_enable_gitpod_path: '/-/profile?user%5Bgitpod_enabled%5D=true',
+  user_profile_enable_gitpod_path: '/-/user_settings/profile?user%5Bgitpod_enabled%5D=true',
 };
 
 export const mockStore = {
diff --git a/spec/helpers/sidebars_helper_spec.rb b/spec/helpers/sidebars_helper_spec.rb
index 0f1484f49db70..cfb7482239e86 100644
--- a/spec/helpers/sidebars_helper_spec.rb
+++ b/spec/helpers/sidebars_helper_spec.rb
@@ -157,7 +157,7 @@
         },
         settings: {
           has_settings: helper.current_user_menu?(:settings),
-          profile_path: profile_path,
+          profile_path: user_settings_profile_path,
           profile_preferences_path: profile_preferences_path
         },
         user_counts: {
@@ -430,7 +430,7 @@
         [
           { title: s_('Navigation|Your work'), link: '/', icon: 'work' },
           public_link,
-          { title: s_('Navigation|Profile'), link: '/-/profile', icon: 'profile' },
+          { title: s_('Navigation|Profile'), link: '/-/user_settings/profile', icon: 'profile' },
           { title: s_('Navigation|Preferences'), link: '/-/profile/preferences', icon: 'preferences' }
         ]
       end
diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb
index c94844eebbc27..0d3108166e105 100644
--- a/spec/helpers/tree_helper_spec.rb
+++ b/spec/helpers/tree_helper_spec.rb
@@ -37,7 +37,7 @@
     let(:blob) { project.repository.blob_at('refs/heads/master', @path) }
 
     let_it_be(:user_preferences_gitpod_path) { '/-/profile/preferences#user_gitpod_enabled' }
-    let_it_be(:user_profile_enable_gitpod_path) { '/-/profile?user%5Bgitpod_enabled%5D=true' }
+    let_it_be(:user_profile_enable_gitpod_path) { '/-/user_settings/profile?user%5Bgitpod_enabled%5D=true' }
 
     before do
       @path = ''
diff --git a/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb
index 8410ba7cfcdb4..efd1099c203c4 100644
--- a/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb
+++ b/spec/lib/sidebars/user_settings/menus/profile_menu_spec.rb
@@ -4,10 +4,10 @@
 
 RSpec.describe Sidebars::UserSettings::Menus::ProfileMenu, feature_category: :navigation do
   it_behaves_like 'User settings menu',
-    link: '/-/profile',
+    link: '/-/user_settings/profile',
     title: _('Profile'),
     icon: 'profile',
-    active_routes: { path: 'profiles#show' }
+    active_routes: { path: 'user_settings/profiles#show' }
 
   it_behaves_like 'User settings menu #render? method'
 end
diff --git a/spec/presenters/user_presenter_spec.rb b/spec/presenters/user_presenter_spec.rb
index fcbadf40bc96b..61f77330478d9 100644
--- a/spec/presenters/user_presenter_spec.rb
+++ b/spec/presenters/user_presenter_spec.rb
@@ -40,7 +40,10 @@
       end
 
       describe '#profile_enable_gitpod_path' do
-        it { expect(presenter.profile_enable_gitpod_path).to eq("/-/profile?user%5Bgitpod_enabled%5D=true") }
+        it do
+          expect(presenter.profile_enable_gitpod_path).to eq(
+            "/-/user_settings/profile?user%5Bgitpod_enabled%5D=true")
+        end
       end
     end
 
diff --git a/spec/requests/legacy_routes_spec.rb b/spec/requests/legacy_routes_spec.rb
index 537ad4054a1cd..3c7128f0eb238 100644
--- a/spec/requests/legacy_routes_spec.rb
+++ b/spec/requests/legacy_routes_spec.rb
@@ -10,6 +10,21 @@
     login_as(user)
   end
 
+  it "GET /-/profile" do
+    get "/-/profile"
+    expect(response).to redirect_to('/-/user_settings/profile')
+  end
+
+  it "PUT /-/profile" do
+    put "/-/profile", params: { user: { pronouns: 'they/them' } }
+    expect(user.reload.pronouns).to eq('they/them')
+  end
+
+  it "PATCH /-/profile" do
+    patch "/-/profile", params: { user: { pronouns: 'they/them' } }
+    expect(user.reload.pronouns).to eq('they/them')
+  end
+
   it "/-/profile/audit_log" do
     get "/-/profile/audit_log"
     expect(response).to redirect_to('/-/user_settings/authentication_log')
diff --git a/spec/requests/robots_txt_spec.rb b/spec/requests/robots_txt_spec.rb
index 18a14677e0cbe..0f7bca9ec8a02 100644
--- a/spec/requests/robots_txt_spec.rb
+++ b/spec/requests/robots_txt_spec.rb
@@ -43,6 +43,7 @@
       '/help',
       '/s/',
       '/-/profile',
+      '/-/user_settings/profile',
       '/-/ide/project',
       '/foo/bar/new',
       '/foo/bar/edit',
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 8b54bc443da30..a300b7ceb7a8d 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -123,8 +123,9 @@
 #             profile_account GET    /-/profile/account(.:format)             profile#account
 #             profile_history GET    /-/profile/history(.:format)             profile#history
 #               profile_token GET    /-/profile/token(.:format)               profile#token
-#                     profile GET    /-/profile(.:format)                     profile#show
-#              profile_update PUT    /-/profile/update(.:format)              profile#update
+#       user_settings_profile GET    /-/user_settings/profile(.:format)       user_settings/profile#show
+#       user_settings_profile PUT    /-/user_settings/profile(.:format)       user_settings/profile#update
+#       user_settings_profile PATCH  /-/user_settings/profile(.:format)       user_settings/profile#update
 RSpec.describe ProfilesController, "routing" do
   it "to #account" do
     expect(get("/-/profile/account")).to route_to('profiles/accounts#show')
@@ -135,7 +136,12 @@
   end
 
   it "to #show" do
-    expect(get("/-/profile")).to route_to('profiles#show')
+    expect(get("/-/user_settings/profile")).to route_to('user_settings/profiles#show')
+  end
+
+  it "to #update" do
+    expect(put("/-/user_settings/profile")).to route_to('user_settings/profiles#update')
+    expect(patch("/-/user_settings/profile")).to route_to('user_settings/profiles#update')
   end
 end
 
diff --git a/spec/views/profiles/show.html.haml_spec.rb b/spec/views/user_settings/profiles/show.html.haml_spec.rb
similarity index 89%
rename from spec/views/profiles/show.html.haml_spec.rb
rename to spec/views/user_settings/profiles/show.html.haml_spec.rb
index 0c5c17c5e3e4e..4c246c301aaa3 100644
--- a/spec/views/profiles/show.html.haml_spec.rb
+++ b/spec/views/user_settings/profiles/show.html.haml_spec.rb
@@ -2,8 +2,8 @@
 
 require 'spec_helper'
 
-RSpec.describe 'profiles/show' do
-  let_it_be(:user_status) { create(:user_status, clear_status_at: 8.hours.from_now) }
+RSpec.describe 'user_settings/profiles/show', feature_category: :user_profile do
+  let_it_be(:user_status) { build_stubbed(:user_status, clear_status_at: 8.hours.from_now) }
   let_it_be(:user) { user_status.user }
 
   before do
-- 
GitLab