Commit 1dd826d4 authored by Bryce Johnson's avatar Bryce Johnson

Make UX upgrades to SignIn/Register views.

- Tab between register and sign in forms
- Add individual input validation error messages
- Validate username
- Update many styles for all login-box forms
parent 602cac52
...@@ -377,6 +377,7 @@ v 8.11.7 ...@@ -377,6 +377,7 @@ v 8.11.7
- Avoid conflict with admin labels when importing GitHub labels. !6158 - Avoid conflict with admin labels when importing GitHub labels. !6158
- Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234 - Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
- Allow the Rails cookie to be used for API authentication. - Allow the Rails cookie to be used for API authentication.
- Login/Register UX upgrade !6328
v 8.11.6 v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005 - Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
Dispatcher = (function() { Dispatcher = (function() {
function Dispatcher() { function Dispatcher() {
this.initSearch(); this.initSearch();
this.initFieldErrors();
this.initPageScripts(); this.initPageScripts();
} }
...@@ -20,6 +21,10 @@ ...@@ -20,6 +21,10 @@
path = page.split(':'); path = page.split(':');
shortcut_handler = null; shortcut_handler = null;
switch (page) { switch (page) {
case 'sessions:new':
case 'sessions:create':
new UsernameValidator();
break;
case 'projects:boards:show': case 'projects:boards:show':
case 'projects:boards:index': case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -291,6 +296,12 @@ ...@@ -291,6 +296,12 @@
} }
}; };
Dispatcher.prototype.initFieldErrors = function() {
$('form.show-gl-field-errors').each(function(i, form) {
new gl.GlFieldErrors(form);
});
};
return Dispatcher; return Dispatcher;
})(); })();
......
((global) => {
/*
* This class overrides the browser's validation error bubbles, displaying custom
* error messages for invalid fields instead. To begin validating any form, add the
* class `show-gl-field-errors` to the form element, and ensure error messages are
* declared in each inputs' title attribute.
*
* Example:
*
* <form class='show-gl-field-errors'>
* <input type='text' name='username' title='Username is required.'/>
*</form>
*
* */
const fieldErrorClass = 'gl-field-error';
const fieldErrorSelector = `.${fieldErrorClass}`;
const inputErrorClass = 'gl-field-error-outline';
class GlFieldErrors {
constructor(form) {
this.form = $(form);
this.initValidators();
}
initValidators () {
this.inputs = this.form.find(':input:not([type=hidden])').toArray();
this.inputs.forEach((input) => {
$(input).off('invalid').on('invalid', this.handleInvalidInput.bind(this));
});
this.form.on('submit', this.catchInvalidFormSubmit);
}
/* Neccessary because Safari & iOS quietly allow form submission when form is invalid */
catchInvalidFormSubmit (event) {
if (!event.currentTarget.checkValidity()) {
event.preventDefault();
// Prevents disabling of invalid submit button by application.js
event.stopPropagation();
}
}
handleInvalidInput (event) {
event.preventDefault();
this.updateFieldValidityState(event);
const $input = $(event.currentTarget);
// For UX, wait til after first invalid submission to check each keyup
$input.off('keyup.field_validator')
.on('keyup.field_validator', this.updateFieldValidityState.bind(this));
}
displayFieldValidity (target, isValid) {
const $input = $(target).removeClass(inputErrorClass);
const $existingError = $input.siblings(fieldErrorSelector);
const alreadyInvalid = !!$existingError.length;
const implicitErrorMessage = $input.attr('title');
const $errorToDisplay = alreadyInvalid ? $existingError.detach() : $(`<p class="${fieldErrorClass}">${implicitErrorMessage}</p>`);
if (!isValid) {
$input.after($errorToDisplay);
$input.addClass(inputErrorClass);
}
this.updateFieldSiblings($errorToDisplay, isValid);
}
updateFieldSiblings($target, isValid) {
const siblings = $target.siblings(`p${fieldErrorSelector}`);
return isValid ? siblings.show() : siblings.hide();
}
checkFieldValidity(target) {
return target.validity.valid;
}
updateFieldValidityState(event) {
const target = event.currentTarget;
const isKeyup = event.type === 'keyup';
const isValid = this.checkFieldValidity(target);
this.displayFieldValidity(target, isValid);
// prevent changing focus while user is typing.
if (!isKeyup) {
this.focusOnFirstInvalid.apply(this);
}
}
focusOnFirstInvalid () {
const firstInvalid = this.inputs.find((input) => !input.validity.valid);
$(firstInvalid).focus();
}
}
global.GlFieldErrors = GlFieldErrors;
})(window.gl || (window.gl = {}));
((global) => {
const debounceTimeoutDuration = 1000;
const inputErrorClass = 'gl-field-error-outline';
const inputSuccessClass = 'gl-field-success-outline';
const messageErrorSelector = '.username .validation-error';
const messageSuccessSelector = '.username .validation-success';
const messagePendingSelector = '.username .validation-pending';
class UsernameValidator {
constructor() {
this.inputElement = $('#new_user_username');
this.inputDomElement = this.inputElement.get(0);
this.available = false;
this.valid = false;
this.pending = false;
this.fresh = true;
this.empty = true;
const debounceTimeout = _.debounce((username) => {
this.validateUsername(username);
}, debounceTimeoutDuration);
this.inputElement.on('keyup.username_check', () => {
const username = this.inputElement.val();
this.valid = this.inputDomElement.validity.valid;
this.fresh = false;
this.empty = !username.length;
if (this.valid) {
return debounceTimeout(username);
}
this.renderState();
});
// Override generic field validation
this.inputElement.on('invalid', this.handleInvalidInput.bind(this));
}
renderState() {
// Clear all state
this.clearFieldValidationState();
if (this.valid && this.available) {
return this.setSuccessState();
}
if (this.empty) {
return this.clearFieldValidationState();
}
if (this.pending) {
return this.setPendingState();
}
if (!this.available) {
return this.setUnavailableState();
}
if (!this.valid) {
return this.setInvalidState();
}
}
handleInvalidInput(event) {
event.preventDefault();
event.stopPropagation();
}
validateUsername(username) {
if (this.valid) {
this.pending = true;
this.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `/u/${username}/exists`,
dataType: 'json',
success: (res) => this.updateValidationState(res.exists)
});
}
}
updateValidationState(usernameTaken) {
if (usernameTaken) {
this.valid = false;
this.available = false;
} else {
this.available = true;
}
this.pending = false;
this.renderState();
}
clearFieldValidationState() {
this.inputElement.siblings('p').hide();
this.inputElement.removeClass(inputErrorClass);
this.inputElement.removeClass(inputSuccessClass);
}
setUnavailableState() {
const $usernameErrorMessage = this.inputElement.siblings(messageErrorSelector);
this.inputElement.addClass(inputErrorClass).removeClass(inputSuccessClass);
$usernameErrorMessage.show();
}
setSuccessState() {
const $usernameSuccessMessage = this.inputElement.siblings(messageSuccessSelector);
this.inputElement.addClass(inputSuccessClass).removeClass(inputErrorClass);
$usernameSuccessMessage.show();
}
setPendingState(show) {
const $usernamePendingMessage = $(messagePendingSelector);
if (this.pending) {
$usernamePendingMessage.show();
} else {
$usernamePendingMessage.hide();
}
}
setInvalidState() {
this.inputElement.addClass(inputErrorClass).removeClass(inputSuccessClass);
$(`.gl-field-error`).show();
}
}
global.UsernameValidator = UsernameValidator;
})(window);
...@@ -152,7 +152,8 @@ ...@@ -152,7 +152,8 @@
@include btn-blue-medium; @include btn-blue-medium;
} }
&.btn-info { &.btn-info,
&.btn-register {
@include btn-blue; @include btn-blue;
} }
......
...@@ -73,8 +73,8 @@ label { ...@@ -73,8 +73,8 @@ label {
} }
.form-control { .form-control {
box-shadow: none; @include box-shadow(none);
border-radius: 3px; border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding; padding: $gl-vert-padding $gl-input-padding;
} }
...@@ -127,3 +127,12 @@ label { ...@@ -127,3 +127,12 @@ label {
border-right: 0; border-right: 0;
} }
} }
.help-block {
margin-bottom: 0;
}
.gl-field-error {
color: $red-normal;
}
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
line-height: 1.5; line-height: 1.5;
p { p {
font-size: 18px;
color: #888; color: #888;
} }
...@@ -36,10 +37,13 @@ ...@@ -36,10 +37,13 @@
} }
} }
p {
font-size: 13px;
}
.login-box { .login-box {
background: #fafafa; box-shadow: 0 0 0 1px $border-color;
border-radius: 10px; border-bottom-right-radius: 2px;
box-shadow: 0 0 2px #ccc; border-bottom-left-radius: 2px;
padding: 15px; padding: 15px;
.login-heading h3 { .login-heading h3 {
...@@ -74,7 +78,6 @@ ...@@ -74,7 +78,6 @@
.nav .active a { .nav .active a {
background: transparent; background: transparent;
} }
}
.form-control { .form-control {
font-size: 14px; font-size: 14px;
...@@ -92,18 +95,109 @@ ...@@ -92,18 +95,109 @@
border-top: 0; border-top: 0;
margin-bottom: 20px; margin-bottom: 20px;
} }
}
// Styles the glowing border of focused input for username async validation
.login-body {
font-size: 13px;
input + p {
margin-top: 5px;
}
.gl-field-success-outline {
border: 1px solid $green-normal;
&:focus {
box-shadow: 0 0 0 1px $green-normal inset, 0 0 4px 0 $green-normal;
border: 0 none;
}
}
.gl-field-error-outline {
border: 1px solid $red-normal;
&:focus {
opacity: .6;
box-shadow: 0 0 0 1px $red-normal inset, 0 0 4px 0 $red-normal;
border: 0 none;
}
}
.username .validation-success,
.gl-field-success-message {
color: $green-normal;
}
.username .validation-error,
.gl-field-error-message {
color: $red-normal;
}
.gl-field-hint {
color: $gl-text-color;
}
}
.new-session-tabs { // Are these being applied to other login-related screens? They need to be.
display: flex;
box-shadow: 0 0 0 1px $border-color;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
li {
flex: 1;
text-align: center;
&.middle { &.middle {
border-top: 0; border-top: 0;
margin-bottom: 0; margin-bottom: 0;
border-radius: 0; border-radius: 0;
&:last-of-type {
border-left: 1px solid $border-color;
}
&:not(.active) {
background-color: $gray-light;
} }
a {
width: 100%;
font-size: 18px;
&:hover {
border: 1px solid transparent;
}
}
&.active {
border-bottom: 1px solid $border-color;
a {
border: none;
border-bottom: 2px solid $link-underline-blue;
color: $black;
&:hover {
border-bottom: 2px solid $link-underline-blue;
}
}
}
}
}
.form-control {
&:active, &:focus { &:active, &:focus {
background-color: #fff; background-color: #fff;
} }
} }
label {
font-weight: normal;
}
.devise-errors { .devise-errors {
h2 { h2 {
margin-top: 0; margin-top: 0;
...@@ -111,14 +205,6 @@ ...@@ -111,14 +205,6 @@
color: #a00; color: #a00;
} }
} }
.remember-me {
margin-top: -10px;
label {
font-weight: normal;
}
}
} }
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
...@@ -137,3 +223,31 @@ ...@@ -137,3 +223,31 @@
height: 32px; height: 32px;
} }
} }
.devise-layout-html {
margin: 0;
padding: 0;
height: 100%;
}
// Fixes footer container to bottom of viewport
.devise-layout-html body {
// offset height of fixed header + 1 to avoid scroll
height: calc(100% - 51px);
margin: 0;
padding: 0;
.page-wrap {
min-height: 100%;
position: relative;
}
.footer-container, hr.footer-fixed {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: $white-light;
}
}
class UsersController < ApplicationController class UsersController < ApplicationController
skip_before_action :authenticate_user! skip_before_action :authenticate_user!
before_action :user before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show] before_action :authorize_read_user!, only: [:show]
def show def show
...@@ -85,6 +85,10 @@ class UsersController < ApplicationController ...@@ -85,6 +85,10 @@ class UsersController < ApplicationController
render 'calendar_activities', layout: false render 'calendar_activities', layout: false
end end
def exists
render json: { exists: !User.find_by_username(params[:username]).nil? }
end
private private
def authorize_read_user! def authorize_read_user!
......
- page_title "Preview | Appearance" = render 'devise/shared/tab_single', { :tab_title => 'Sign in preview' }
.login-box .login-box
.login-heading %form.show-gl-field-errors
%h3 Existing user? Sign in .form-group
%form = label_tag :login
= text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email" = text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
= password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password" .form-group
= label_tag :password
= password_field_tag :password, nil, class: "form-control bottom", title: 'This field is required.'
.form-group
= button_tag "Sign in", class: "btn-create btn" = button_tag "Sign in", class: "btn-create btn"
= render 'devise/shared/tab_single', { :tab_title => 'Resend confirmation instructions' }
.login-box .login-box
.login-heading
%h3 Resend confirmation instructions
.login-body .login-body
= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| = form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.clearfix.append-bottom-20 .form-group
= f.email_field :email, placeholder: 'Email', class: "form-control", required: true = f.label :email
= f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.'
.clearfix .clearfix
= f.submit "Resend confirmation instructions", class: 'btn btn-success' = f.submit "Resend", class: 'btn btn-success'
.clearfix.prepend-top-20 .clearfix.prepend-top-20
= render 'devise/shared/sign_in_link' = render 'devise/shared/sign_in_link'
= render 'devise/shared/tab_single', { :tab_title => 'Change your password' }
.login-box .login-box
.login-heading
%h3 Change your password
.login-body .login-body
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put, class: 'show-gl-field-errors' }) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
= f.hidden_field :reset_password_token = f.hidden_field :reset_password_token
%div .form-group
= f.password_field :password, class: "form-control top", placeholder: "New password", required: true = f.label 'New password', for: :password
%div = f.password_field :password, class: "form-control top", required: true, title: 'This field is required'
= f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true .form-group
= f.label 'Confirm new password', for: :password_confirmation
= f.password_field :password_confirmation, class: "form-control bottom", title: 'This field is required', required: true
.clearfix .clearfix
= f.submit "Change your password", class: "btn btn-primary" = f.submit "Change your password", class: "btn btn-primary"
.clearfix.prepend-top-20 .clearfix.prepend-top-20
%p %p
= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name) %span.light Didn't receive a confirmation email?
= render 'devise/shared/sign_in_link' = link_to "Request a new one", new_confirmation_path(resource_name)
= render 'devise/shared/sign_in_link'
= render 'devise/shared/tab_single', { :tab_title => 'Reset Password' }
.login-box .login-box
.login-heading
%h3 Reset password
.login-body .login-body
= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| = form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.clearfix.append-bottom-20 .form-group
= f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true = f.label :email
= f.email_field :email, class: "form-control", required: true, value: params[:user_email], autofocus: true, title: 'Please provide a valid email address.'
.clearfix .clearfix
= f.submit "Reset password", class: "btn-primary btn" = f.submit "Reset password", class: "btn-primary btn"
......
= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| = form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f|
= f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off" %div.form-group
= f.password_field :password, class: "form-control bottom", placeholder: "Password" = f.label "Username or email", for: :login
= f.text_field :login, class: "form-control top", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off", required: true, title: "This field is required."
%div.form-group
= f.label :password
= f.password_field :password, class: "form-control bottom", required: true, title: "This field is required."
.sign-in .sign-in
= f.submit "Sign in", class: "btn btn-save" = f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
......
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do = form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' class: 'show-gl-field-errors') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"} .form-group
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} = label_tag 'Username or email', for: :username
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
%label{for: "remember_me"} %label{for: "remember_me"}
......
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do = form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"} .form-group
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"} = label_tag "#{server['label']} Login", for: :username
= text_field_tag :username, nil, {class: "form-control top", title: "This field is required.", autofocus: "autofocus", required: true }
.form-group
= label_tag :password
= password_field_tag :password, nil, { class: "form-control bottom", title: "This field is required.", required: true }
- if devise_mapping.rememberable? - if devise_mapping.rememberable?
.remember-me.checkbox .remember-me.checkbox
%label{for: "remember_me"} %label{for: "remember_me"}
......
- page_title "Sign in" - page_title "Sign in"
%div %div
- if form_based_providers.any?
= render 'devise/shared/tabs_ldap'
- else
= render 'devise/shared/tabs_normal'
.tab-content
- if signin_enabled? || ldap_enabled? || crowd_enabled? - if signin_enabled? || ldap_enabled? || crowd_enabled?
= render 'devise/shared/signin_box' = render 'devise/shared/signin_box'
-# Omniauth fits between signin/ldap signin and signup and does not have a surrounding box
- if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
.clearfix.prepend-top-20
= render 'devise/shared/omniauth_box'
-# Signup only makes sense if you can also sign-in -# Signup only makes sense if you can also sign-in
- if signin_enabled? && signup_enabled? - if signin_enabled? && signup_enabled?
.prepend-top-20
= render 'devise/shared/signup_box' = render 'devise/shared/signup_box'
- if omniauth_enabled? && devise_mapping.omniauthable? && button_based_providers_enabled?
.clearfix
= render 'devise/shared/omniauth_box'
-# Show a message if none of the mechanisms above are enabled -# Show a message if none of the mechanisms above are enabled
- if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?) - if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div %div
No authentication methods configured. No authentication methods configured.
...@@ -3,20 +3,19 @@ ...@@ -3,20 +3,19 @@
= page_specific_javascript_tag('u2f.js') = page_specific_javascript_tag('u2f.js')
%div %div
= render 'devise/shared/tab_single', { :tab_title => 'Two-Factor Authentication' }
.login-box .login-box
.login-heading
%h3 Two-Factor Authentication
.login-body .login-body
- if @user.two_factor_otp_enabled? - if @user.two_factor_otp_enabled?
%h5 Authenticate via Two-Factor App = form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post) do |f|
- resource_params = params[resource_name].presence || params - resource_params = params[resource_name].presence || params
= f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0) = f.hidden_field :remember_me, value: resource_params.fetch(:remember_me, 0)
= f.text_field :otp_attempt, class: 'form-control', placeholder: 'Two-Factor Authentication code', required: true, autofocus: true, autocomplete: 'off' .form-group
= f.label 'Two-Factor Authentication code', name: :otp_attempt
= f.text_field :otp_attempt, class: 'form-control', required: true, autofocus: true, autocomplete: 'off', title: 'This field is required.'
%p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes. %p.help-block.hint Enter the code from the two-factor app on your mobile device. If you've lost your device, you may enter one of your recovery codes.
.prepend-top-20 .prepend-top-20
= f.submit "Verify code", class: "btn btn-save" = f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled? - if @user.two_factor_u2f_enabled?
%hr
= render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name } = render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
%p %div.login-box
%p
%span.light %span.light
Sign in with &nbsp; Sign in with &nbsp;
- providers = enabled_button_based_providers - providers = enabled_button_based_providers
......
%p %p
%span.light %span.light
Already have login and password? Already have login and password?
%strong
= link_to "Sign in", new_session_path(resource_name) = link_to "Sign in", new_session_path(resource_name)
.login-box #login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' }
- if signup_enabled?
.login-heading
%h3 Existing user? Sign in
- else
.login-heading
%h3 Sign in
.login-body .login-body
- if form_based_providers.any? - if form_based_providers.any?
%ul.nav-links
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li{class: (:active if i.zero? && !crowd_enabled?)}
= link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
.tab-content
- if crowd_enabled? - if crowd_enabled?
%div.tab-pane.active{id: "tab-crowd"} %div.tab-pane.active{id: "tab-crowd"}
= render 'devise/sessions/new_crowd' = render 'devise/sessions/new_crowd'
......
.login-box #register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
- if signin_enabled?
.login-heading
%h3 New user? Create an account
- else
.login-heading
%h3 Create an account
.login-body .login-body
= form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name)) do |f| = form_for(resource, as: "new_#{resource_name}", url: registration_path(resource_name), html: { class: "new_new_user show-gl-field-errors", "aria-live" => "assertive" }) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
%div %div.form-group
= f.text_field :name, class: "form-control top", placeholder: "Name", required: true = f.label :name
%div = f.text_field :name, class: "form-control top", required: true, title: "This field is required."
= f.text_field :username, class: "form-control middle", placeholder: "Username", required: true %div.username.form-group
%div = f.label :username
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true = f.text_field :username, class: "form-control middle", pattern: "[a-zA-Z0-9]+", required: true
%p.gl-field-error.hide Please create a username with only alphanumeric characters.
%p.validation-error.hide Username is already taken.
%p.validation-success.hide Username is available.
%p.validation-pending.hide Checking username availability...
%div.form-group
= f.label :email
= f.email_field :email, class: "form-control middle", required: true, title: "Please provide a valid email address."
.form-group.append-bottom-20#password-strength .form-group.append-bottom-20#password-strength
= f.password_field :password, class: "form-control bottom", placeholder: "Password - minimum length #{@minimum_password_length} characters", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters" = f.label :password
= f.password_field :password, class: "form-control bottom", required: true, pattern: ".{#{@minimum_password_length},}", title: "Minimum length is #{@minimum_password_length} characters."
%p.gl-field-hint Minimum length is #{@minimum_password_length} characters
%div %div
- if current_application_settings.recaptcha_enabled - if current_application_settings.recaptcha_enabled
= recaptcha_tags = recaptcha_tags
%div %div
= f.submit "Sign up", class: "btn-create btn" = f.submit "Register", class: "btn-register btn"
.clearfix.prepend-top-20 .clearfix.prepend-top-20
%p %p
%span.light Didn't receive a confirmation email? %span.light Didn't receive a confirmation email?
......
// = render 'devise/shared/tab_single', :tab_title => 'Tab Title'
%ul.nav-links.nav-tabs.new-session-tabs.single-tab
%li.active
= link_to tab_title, '#', disabled: true
%ul.new-session-tabs.nav-links.nav-tabs
- if crowd_enabled?
%li.active
= link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li{class: (:active if i.zero? && !crowd_enabled?)}
= link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
%ul.nav-links.new-session-tabs.nav-tabs{ role: 'tablist'}
%li.active{ role: 'presentation' }
%a{ href: '#login-pane', data: {'toggle':'tab'}, role: 'tab'} Sign in
%li{ role: 'presentation'}
%a{ href: '#register-pane', data: {'toggle':'tab'}, role: 'tab'} Register
= render 'devise/shared/tab_single', { :tab_title => 'Resend unlock instructions' }
.login-box .login-box
.login-heading
%h3 Resend unlock email
.login-body .login-body
= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'show-gl-field-errors' }) do |f|
.devise-errors .devise-errors
= devise_error_messages! = devise_error_messages!
.clearfix.append-bottom-20 .form-group.append-bottom-20
= f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off' = f.label :email
= f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.'
.clearfix .clearfix
= f.submit 'Resend unlock instructions', class: 'btn btn-success' = f.submit 'Resend unlock instructions', class: 'btn btn-success'
......
!!! 5 !!! 5
%html{ lang: "en"} %html{ lang: "en", class: "devise-layout-html"}
= render "layouts/head" = render "layouts/head"
%body.ui_charcoal.login-page.application.navless %body{ class: "ui_charcoal login-page application navless", data: {page: body_data_page}}
.page-wrap
= Gon::Base.render_data = Gon::Base.render_data
= render "layouts/header/empty" = render "layouts/header/empty"
= render "layouts/broadcast" = render "layouts/broadcast"
...@@ -9,7 +10,7 @@ ...@@ -9,7 +10,7 @@
.content .content
= render "layouts/flash" = render "layouts/flash"
.row .row
.col-sm-5.pull-right .col-sm-5.pull-right.new-session-forms-container
= yield = yield
.col-sm-7.brand-holder.pull-left .col-sm-7.brand-holder.pull-left
%h1 %h1
...@@ -28,8 +29,8 @@ ...@@ -28,8 +29,8 @@
- if current_application_settings.sign_in_text.present? - if current_application_settings.sign_in_text.present?
= markdown_field(current_application_settings, :sign_in_text) = markdown_field(current_application_settings, :sign_in_text)
%hr %hr.footer-fixed
.container .container.footer-container
.footer-links .footer-links
= link_to "Explore", explore_root_path = link_to "Explore", explore_root_path
= link_to "Help", help_path = link_to "Help", help_path
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
%script#js-authenticate-u2f-setup{ type: "text/template" } %script#js-authenticate-u2f-setup{ type: "text/template" }
%div %div
%p Insert your security key (if you haven't already), and press the button below. %p Insert your security key (if you haven't already), and press the button below.
%a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Login Via U2F Device %a.btn.btn-info#js-login-u2f-device{ href: 'javascript:void(0)' } Sign in via U2F device
%script#js-authenticate-u2f-in-progress{ type: "text/template" } %script#js-authenticate-u2f-in-progress{ type: "text/template" }
%p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now. %p Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.
......
This diff is collapsed.
...@@ -14,7 +14,7 @@ feature 'Signup', feature: true do ...@@ -14,7 +14,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: user.password
click_button "Sign up" click_button "Register"
expect(current_path).to eq users_almost_there_path expect(current_path).to eq users_almost_there_path
expect(page).to have_content("Please check your email to confirm your account") expect(page).to have_content("Please check your email to confirm your account")
...@@ -33,7 +33,7 @@ feature 'Signup', feature: true do ...@@ -33,7 +33,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: user.password
click_button "Sign up" click_button "Register"
expect(current_path).to eq dashboard_projects_path expect(current_path).to eq dashboard_projects_path
expect(page).to have_content("Welcome! You have signed up successfully.") expect(page).to have_content("Welcome! You have signed up successfully.")
...@@ -52,7 +52,7 @@ feature 'Signup', feature: true do ...@@ -52,7 +52,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: user.password
click_button "Sign up" click_button "Register"
expect(current_path).to eq user_registration_path expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved") expect(page).to have_content("error prohibited this user from being saved")
...@@ -69,7 +69,7 @@ feature 'Signup', feature: true do ...@@ -69,7 +69,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password fill_in 'new_user_password', with: user.password
click_button "Sign up" click_button "Register"
expect(current_path).to eq user_registration_path expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/) expect(page.body).not_to match(/#{user.password}/)
......
...@@ -160,7 +160,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -160,7 +160,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user) login_with(user)
@u2f_device.respond_to_u2f_authentication @u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
...@@ -174,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -174,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user) login_with(user)
@u2f_device.respond_to_u2f_authentication @u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
...@@ -186,7 +186,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -186,7 +186,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user, remember: true) login_with(user, remember: true)
@u2f_device.respond_to_u2f_authentication @u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
within 'div#js-authenticate-u2f' do within 'div#js-authenticate-u2f' do
...@@ -209,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -209,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the old U2F device # Try authenticating user with the old U2F device
login_as(current_user) login_as(current_user)
@u2f_device.respond_to_u2f_authentication @u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
...@@ -230,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -230,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the same U2F device # Try authenticating user with the same U2F device
login_as(current_user) login_as(current_user)
@u2f_device.respond_to_u2f_authentication @u2f_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
...@@ -244,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -244,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name) unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user) login_as(user)
unregistered_device.respond_to_u2f_authentication unregistered_device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
...@@ -271,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature: ...@@ -271,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
[first_device, second_device].each do |device| [first_device, second_device].each do |device|
login_as(user) login_as(user)
device.respond_to_u2f_authentication device.respond_to_u2f_authentication
click_on "Login Via U2F Device" click_on "Sign in via U2F device"
expect(page.body).to match('We heard back from your U2F device') expect(page.body).to match('We heard back from your U2F device')
click_on "Authenticate via U2F Device" click_on "Authenticate via U2F Device"
......
require 'spec_helper' require 'spec_helper'
feature 'Users', feature: true do feature 'Users', feature: true, js: true do
let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'GET /users/sign_in creates a new user account' do scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Name Surname' fill_in 'new_user_name', with: 'Name Surname'
fill_in 'new_user_username', with: 'Great' fill_in 'new_user_username', with: 'Great'
fill_in 'new_user_email', with: 'name@mail.com' fill_in 'new_user_email', with: 'name@mail.com'
fill_in 'new_user_password', with: 'password1234' fill_in 'new_user_password', with: 'password1234'
expect { click_button 'Sign up' }.to change { User.count }.by(1) expect { click_button 'Register' }.to change { User.count }.by(1)
end end
scenario 'Successful user signin invalidates password reset token' do scenario 'Successful user signin invalidates password reset token' do
...@@ -31,11 +32,12 @@ feature 'Users', feature: true do ...@@ -31,11 +32,12 @@ feature 'Users', feature: true do
scenario 'Should show one error if email is already taken' do scenario 'Should show one error if email is already taken' do
visit new_user_session_path visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Another user name' fill_in 'new_user_name', with: 'Another user name'
fill_in 'new_user_username', with: 'anotheruser' fill_in 'new_user_username', with: 'anotheruser'
fill_in 'new_user_email', with: user.email fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: '12341234' fill_in 'new_user_password', with: '12341234'
expect { click_button 'Sign up' }.to change { User.count }.by(0) expect { click_button 'Register' }.to change { User.count }.by(0)
expect(page).to have_text('Email has already been taken') expect(page).to have_text('Email has already been taken')
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end end
...@@ -51,6 +53,30 @@ feature 'Users', feature: true do ...@@ -51,6 +53,30 @@ feature 'Users', feature: true do
end end
end end
feature 'username validation' do
include WaitForAjax
let(:loading_icon) { '.fa.fa-spinner' }
let(:username_input) { 'new_user_username' }
before(:each) do
visit new_user_session_path
click_link 'Register'
@username_field = find '.username'
end
scenario 'shows an error border if the username already exists' do
fill_in username_input, with: user.username
wait_for_ajax
expect(@username_field).to have_css '.gl-field-error-outline'
end
scenario 'doesn\'t show an error border if the username is available' do
fill_in username_input, with: 'new-user'
wait_for_ajax
expect(@username_field).not_to have_css '.gl-field-error-outline'
end
end
def errors_on_page(page) def errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n") page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
end end
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
setupButton = this.container.find("#js-login-u2f-device"); setupButton = this.container.find("#js-login-u2f-device");
setupMessage = this.container.find("p"); setupMessage = this.container.find("p");
expect(setupMessage.text()).toContain('Insert your security key'); expect(setupMessage.text()).toContain('Insert your security key');
expect(setupButton.text()).toBe('Login Via U2F Device'); expect(setupButton.text()).toBe('Sign in via U2F device');
setupButton.trigger('click'); setupButton.trigger('click');
inProgressMessage = this.container.find("p"); inProgressMessage = this.container.find("p");
expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); expect(inProgressMessage.text()).toContain("Trying to communicate with your device");
......
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