Commit 3d3fa624 authored by Dmytro Zaporozhets's avatar Dmytro Zaporozhets Committed by Ezekiel Kigbo

Require namespace path to be 2 chars long

This will affect Group URL and username (in URL).

This will allow to keep one letter routes for service needs.
Also it should improve autocomplete quality. Also most of 1
and 2 character length usernames are usually taken by bots.
Signed-off-by: default avatarDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
parent 530ddc69
...@@ -114,7 +114,7 @@ export default class GlFieldError { ...@@ -114,7 +114,7 @@ export default class GlFieldError {
this.state.empty = currentValue === ''; this.state.empty = currentValue === '';
this.state.submitted = true; this.state.submitted = true;
this.renderValidity(); this.renderValidity();
this.form.focusOnFirstInvalid.apply(this.form); this.form.focusInvalid.apply(this.form);
// For UX, wait til after first invalid submission to check each keyup // For UX, wait til after first invalid submission to check each keyup
this.inputElement this.inputElement
......
...@@ -52,10 +52,23 @@ export default class GlFieldErrors { ...@@ -52,10 +52,23 @@ export default class GlFieldErrors {
}); });
} }
focusOnFirstInvalid() { get invalidInputs() {
const firstInvalid = this.state.inputs.filter( return this.state.inputs.filter(
input => !input.inputDomElement.validity.valid, ({
)[0]; inputDomElement: {
firstInvalid.inputElement.focus(); validity: { valid },
},
}) => !valid,
);
}
get focusedInvalidInput() {
return this.invalidInputs.find(({ inputElement }) => inputElement.is(':focus'));
}
focusInvalid() {
if (this.focusedInvalidInput) return;
this.invalidInputs[0].inputElement.focus();
} }
} }
...@@ -21,11 +21,24 @@ export default class LengthValidator extends InputValidator { ...@@ -21,11 +21,24 @@ export default class LengthValidator extends InputValidator {
); );
const { value } = this.inputDomElement; const { value } = this.inputDomElement;
const { maxLengthMessage, maxLength } = this.inputDomElement.dataset; const {
minLength,
minLengthMessage,
maxLengthMessage,
maxLength,
} = this.inputDomElement.dataset;
this.invalidInput = false;
if (value.length > parseInt(maxLength, 10)) {
this.invalidInput = true;
this.errorMessage = maxLengthMessage; this.errorMessage = maxLengthMessage;
}
this.invalidInput = value.length > parseInt(maxLength, 10); if (value.length < parseInt(minLength, 10)) {
this.invalidInput = true;
this.errorMessage = minLengthMessage;
}
this.setValidationStateAndMessage(); this.setValidationStateAndMessage();
} }
......
...@@ -39,7 +39,7 @@ export default class UsernameValidator extends InputValidator { ...@@ -39,7 +39,7 @@ export default class UsernameValidator extends InputValidator {
static validateUsernameInput(inputDomElement) { static validateUsernameInput(inputDomElement) {
const username = inputDomElement.value; const username = inputDomElement.value;
if (inputDomElement.checkValidity() && username.length > 0) { if (inputDomElement.checkValidity() && username.length > 1) {
UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector); UsernameValidator.setMessageVisibility(inputDomElement, pendingMessageSelector);
UsernameValidator.fetchUsernameAvailability(username) UsernameValidator.fetchUsernameAvailability(username)
.then(usernameTaken => { .then(usernameTaken => {
......
...@@ -48,6 +48,13 @@ class Namespace < ApplicationRecord ...@@ -48,6 +48,13 @@ class Namespace < ApplicationRecord
length: { maximum: 255 }, length: { maximum: 255 },
namespace_path: true namespace_path: true
# Introduce minimal path length of 2 characters.
# Allow change of other attributes without forcing users to
# rename their user or group. At the same time prevent changing
# the path without complying with new 2 chars requirement.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/225214
validates :path, length: { minimum: 2 }, if: :path_changed?
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true } validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
validate :nesting_level_allowed validate :nesting_level_allowed
......
- max_first_name_length = max_last_name_length = 127 - max_first_name_length = max_last_name_length = 127
- max_username_length = 255 - max_username_length = 255
- min_username_length = 2
.signup-box.p-3.mb-2 .signup-box.p-3.mb-2
.signup-body .signup-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
...@@ -16,7 +17,7 @@ ...@@ -16,7 +17,7 @@
= f.text_field :last_name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_last_name_length, :max_length_message => _("Last Name is too long (maximum is %{max_length} characters).") % { max_length: max_last_name_length }, :qa_selector => 'new_user_lastname_field' }, required: true, title: _("This field is required.") = f.text_field :last_name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_last_name_length, :max_length_message => _("Last Name is too long (maximum is %{max_length} characters).") % { max_length: max_last_name_length }, :qa_selector => 'new_user_lastname_field' }, required: true, title: _("This field is required.")
.username.form-group .username.form-group
= f.label :username, class: 'label-bold' = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => _("Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") = f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :min_length => min_username_length, :min_length_message => s_("SignUp|Username is too short (minimum is %{min_length} characters).") % { min_length: min_username_length }, :max_length => max_username_length, :max_length_message => _("Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
%p.validation-error.gl-field-error-ignore.field-validation.mt-1.hide.cred= _('Username is already taken.') %p.validation-error.gl-field-error-ignore.field-validation.mt-1.hide.cred= _('Username is already taken.')
%p.validation-success.gl-field-error-ignore.field-validation.mt-1.hide.cgreen= _('Username is available.') %p.validation-success.gl-field-error-ignore.field-validation.mt-1.hide.cgreen= _('Username is available.')
%p.validation-pending.gl-field-error-ignore.field-validation.mt-1.hide= _('Checking username availability...') %p.validation-pending.gl-field-error-ignore.field-validation.mt-1.hide= _('Checking username availability...')
......
- max_name_length = 255 - max_name_length = 255
- max_username_length = 255 - max_username_length = 255
- min_username_length = 2
#register-pane.tab-pane.login-box{ role: 'tabpanel' } #register-pane.tab-pane.login-box{ role: 'tabpanel' }
.login-body .login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f| = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user gl-show-field-errors", "aria-live" => "assertive" }) do |f|
...@@ -12,7 +13,7 @@ ...@@ -12,7 +13,7 @@
= f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.") = f.text_field :name, class: "form-control top js-block-emoji js-validate-length", :data => { :max_length => max_name_length, :max_length_message => s_("SignUp|Name is too long (maximum is %{max_length} characters).") % { max_length: max_name_length }, :qa_selector => 'new_user_name_field' }, required: true, title: _("This field is required.")
.username.form-group .username.form-group
= f.label :username, class: 'label-bold' = f.label :username, class: 'label-bold'
= f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.") = f.text_field :username, class: "form-control middle js-block-emoji js-validate-length js-validate-username", :data => { :min_length => min_username_length, :min_length_message => s_("SignUp|Username is too short (minimum is %{min_length} characters).") % { min_length: min_username_length }, :max_length => max_username_length, :max_length_message => s_("SignUp|Username is too long (maximum is %{max_length} characters).") % { max_length: max_username_length }, :qa_selector => 'new_user_username_field' }, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, required: true, title: _("Please create a username with only alphanumeric characters.")
%p.validation-error.gl-field-error-ignore.field-validation.hide= _('Username is already taken.') %p.validation-error.gl-field-error-ignore.field-validation.hide= _('Username is already taken.')
%p.validation-success.gl-field-error-ignore.field-validation.hide= _('Username is available.') %p.validation-success.gl-field-error-ignore.field-validation.hide= _('Username is available.')
%p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking username availability...') %p.validation-pending.gl-field-error-ignore.field-validation.hide= _('Checking username availability...')
......
---
title: Require namespace path (and username) to be at least 2 chars long
merge_request: 35649
author:
type: changed
...@@ -999,8 +999,8 @@ RSpec.describe User do ...@@ -999,8 +999,8 @@ RSpec.describe User do
describe '#managed_free_namespaces' do describe '#managed_free_namespaces' do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:licensed_group) { create(:group, gitlab_subscription: create(:gitlab_subscription, :bronze)) } let_it_be(:licensed_group) { create(:group, gitlab_subscription: create(:gitlab_subscription, :bronze)) }
let_it_be(:free_group_z) { create(:group, name: 'Z', gitlab_subscription: create(:gitlab_subscription, :free)) } let_it_be(:free_group_z) { create(:group, name: 'AZ', gitlab_subscription: create(:gitlab_subscription, :free)) }
let_it_be(:free_group_a) { create(:group, name: 'A', gitlab_subscription: create(:gitlab_subscription, :free)) } let_it_be(:free_group_a) { create(:group, name: 'AA', gitlab_subscription: create(:gitlab_subscription, :free)) }
subject { user.managed_free_namespaces } subject { user.managed_free_namespaces }
......
...@@ -21013,6 +21013,9 @@ msgstr "" ...@@ -21013,6 +21013,9 @@ msgstr ""
msgid "SignUp|Username is too long (maximum is %{max_length} characters)." msgid "SignUp|Username is too long (maximum is %{max_length} characters)."
msgstr "" msgstr ""
msgid "SignUp|Username is too short (minimum is %{min_length} characters)."
msgstr ""
msgid "Signed in" msgid "Signed in"
msgstr "" msgstr ""
......
...@@ -70,6 +70,13 @@ RSpec.shared_examples 'Signup' do ...@@ -70,6 +70,13 @@ RSpec.shared_examples 'Signup' do
expect(page).to have_content("Username is too long (maximum is 255 characters).") expect(page).to have_content("Username is too long (maximum is 255 characters).")
end end
it 'shows an error message if the username is less than 2 characters' do
fill_in 'new_user_username', with: 'u'
wait_for_requests
expect(page).to have_content("Username is too short (minimum is 2 characters).")
end
it 'shows an error message on submit if the username contains special characters' do it 'shows an error message on submit if the username contains special characters' do
fill_in 'new_user_username', with: 'new$user!username' fill_in 'new_user_username', with: 'new$user!username'
wait_for_requests wait_for_requests
......
...@@ -65,6 +65,36 @@ RSpec.describe Namespace do ...@@ -65,6 +65,36 @@ RSpec.describe Namespace do
it { expect(group).to be_valid } it { expect(group).to be_valid }
end end
end end
describe '1 char path length' do
it 'does not allow to create one' do
namespace = build(:namespace, path: 'j')
expect(namespace).not_to be_valid
expect(namespace.errors[:path].first).to eq('is too short (minimum is 2 characters)')
end
it 'does not allow to update one' do
namespace = create(:namespace)
namespace.update(path: 'j')
expect(namespace).not_to be_valid
expect(namespace.errors[:path].first).to eq('is too short (minimum is 2 characters)')
end
it 'allows updating other attributes for existing record' do
namespace = build(:namespace, path: 'j')
namespace.save(validate: false)
namespace.reload
expect(namespace.path).to eq('j')
namespace.update(name: 'something new')
expect(namespace).to be_valid
expect(namespace.name).to eq('something new')
end
end
end end
describe 'delegate' do describe 'delegate' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment