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
- Avoid conflict with admin labels when importing GitHub labels. !6158
- Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
- Allow the Rails cookie to be used for API authentication.
- Login/Register UX upgrade !6328
v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
......
......@@ -8,6 +8,7 @@
Dispatcher = (function() {
function Dispatcher() {
this.initSearch();
this.initFieldErrors();
this.initPageScripts();
}
......@@ -20,6 +21,10 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
case 'sessions:new':
case 'sessions:create':
new UsernameValidator();
break;
case 'projects:boards:show':
case 'projects:boards:index':
shortcut_handler = new ShortcutsNavigation();
......@@ -291,6 +296,12 @@
}
};
Dispatcher.prototype.initFieldErrors = function() {
$('form.show-gl-field-errors').each(function(i, form) {
new gl.GlFieldErrors(form);
});
};
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 @@
@include btn-blue-medium;
}
&.btn-info {
&.btn-info,
&.btn-register {
@include btn-blue;
}
......
......@@ -73,8 +73,8 @@ label {
}
.form-control {
box-shadow: none;
border-radius: 3px;
@include box-shadow(none);
border-radius: 2px;
padding: $gl-vert-padding $gl-input-padding;
}
......@@ -127,3 +127,12 @@ label {
border-right: 0;
}
}
.help-block {
margin-bottom: 0;
}
.gl-field-error {
color: $red-normal;
}
......@@ -17,6 +17,7 @@
line-height: 1.5;
p {
font-size: 18px;
color: #888;
}
......@@ -36,10 +37,13 @@
}
}
p {
font-size: 13px;
}
.login-box {
background: #fafafa;
border-radius: 10px;
box-shadow: 0 0 2px #ccc;
box-shadow: 0 0 0 1px $border-color;
border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
padding: 15px;
.login-heading h3 {
......@@ -74,7 +78,6 @@
.nav .active a {
background: transparent;
}
}
.form-control {
font-size: 14px;
......@@ -92,18 +95,109 @@
border-top: 0;
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 {
border-top: 0;
margin-bottom: 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 {
background-color: #fff;
}
}
label {
font-weight: normal;
}
.devise-errors {
h2 {
margin-top: 0;
......@@ -111,14 +205,6 @@
color: #a00;
}
}
.remember-me {
margin-top: -10px;
label {
font-weight: normal;
}
}
}
@media (max-width: $screen-xs-max) {
......@@ -137,3 +223,31 @@
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
skip_before_action :authenticate_user!
before_action :user
before_action :user, except: [:exists]
before_action :authorize_read_user!, only: [:show]
def show
......@@ -85,6 +85,10 @@ class UsersController < ApplicationController
render 'calendar_activities', layout: false
end
def exists
render json: { exists: !User.find_by_username(params[:username]).nil? }
end
private
def authorize_read_user!
......
- page_title "Preview | Appearance"
= render 'devise/shared/tab_single', { :tab_title => 'Sign in preview' }
.login-box
.login-heading
%h3 Existing user? Sign in
%form
= text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
= password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
%form.show-gl-field-errors
.form-group
= label_tag :login
= text_field_tag :login, nil, class: "form-control top", title: 'Please provide your username or email address.'
.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"
= render 'devise/shared/tab_single', { :tab_title => 'Resend confirmation instructions' }
.login-box
.login-heading
%h3 Resend confirmation instructions
.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_error_messages!
.clearfix.append-bottom-20
= f.email_field :email, placeholder: 'Email', class: "form-control", required: true
.form-group
= f.label :email
= f.email_field :email, class: "form-control", required: true, title: 'Please provide a valid email address.'
.clearfix
= f.submit "Resend confirmation instructions", class: 'btn btn-success'
= f.submit "Resend", class: 'btn btn-success'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
= render 'devise/shared/tab_single', { :tab_title => 'Change your password' }
.login-box
.login-heading
%h3 Change your password
.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_error_messages!
= f.hidden_field :reset_password_token
%div
= f.password_field :password, class: "form-control top", placeholder: "New password", required: true
%div
= f.password_field :password_confirmation, class: "form-control bottom", placeholder: "Confirm new password", required: true
.form-group
= f.label 'New password', for: :password
= f.password_field :password, class: "form-control top", required: true, title: 'This field is required'
.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
= f.submit "Change your password", class: "btn btn-primary"
.clearfix.prepend-top-20
%p
= link_to "Didn't receive confirmation instructions?", new_confirmation_path(resource_name)
= render 'devise/shared/sign_in_link'
%span.light Didn't receive a confirmation email?
= 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-heading
%h3 Reset password
.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_error_messages!
.clearfix.append-bottom-20
= f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true
.form-group
= 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
= f.submit "Reset password", class: "btn-primary btn"
......
= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f|
= f.text_field :login, class: "form-control top", placeholder: "Username or Email", autofocus: "autofocus", autocapitalize: "off", autocorrect: "off"
= f.password_field :password, class: "form-control bottom", placeholder: "Password"
= form_for(resource, as: resource_name, url: session_path(resource_name), html: { class: 'new_user show-gl-field-errors', 'aria-live' => 'assertive'}) do |f|
%div.form-group
= 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
= f.submit "Sign in", class: "btn btn-save"
- if devise_mapping.rememberable?
......
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' ) do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
= form_tag(omniauth_authorize_path(:user, :crowd), id: 'new_crowd_user' class: 'show-gl-field-errors') do
.form-group
= 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?
.remember-me.checkbox
%label{for: "remember_me"}
......
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user', class: "show-gl-field-errors") do
.form-group
= 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?
.remember-me.checkbox
%label{for: "remember_me"}
......
- page_title "Sign in"
%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?
= 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
- if signin_enabled? && signup_enabled?
.prepend-top-20
= 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
- if !signin_enabled? && !ldap_enabled? && !(omniauth_enabled? && devise_mapping.omniauthable?)
%div
No authentication methods configured.
......@@ -3,20 +3,19 @@
= page_specific_javascript_tag('u2f.js')
%div
= render 'devise/shared/tab_single', { :tab_title => 'Two-Factor Authentication' }
.login-box
.login-heading
%h3 Two-Factor Authentication
.login-body
- 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) do |f|
= form_for(resource, as: resource_name, url: session_path(resource_name), method: :post, html: { class: 'edit_user show-gl-field-errors' }) do |f|
- resource_params = params[resource_name].presence || params
= 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.
.prepend-top-20
= f.submit "Verify code", class: "btn btn-save"
- if @user.two_factor_u2f_enabled?
%hr
= render "u2f/authenticate", locals: { params: params, resource: resource, resource_name: resource_name }
%p
%div.login-box
%p
%span.light
Sign in with &nbsp;
- providers = enabled_button_based_providers
......
%p
%span.light
Already have login and password?
%strong
= link_to "Sign in", new_session_path(resource_name)
.login-box
- if signup_enabled?
.login-heading
%h3 Existing user? Sign in
- else
.login-heading
%h3 Sign in
#login-pane.login-box{ role: 'tabpanel', class: 'tab-pane active' }
.login-body
- 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?
%div.tab-pane.active{id: "tab-crowd"}
= render 'devise/sessions/new_crowd'
......
.login-box
- if signin_enabled?
.login-heading
%h3 New user? Create an account
- else
.login-heading
%h3 Create an account
#register-pane.login-box{ role: 'tabpanel', class: 'tab-pane' }
.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_error_messages!
%div
= f.text_field :name, class: "form-control top", placeholder: "Name", required: true
%div
= f.text_field :username, class: "form-control middle", placeholder: "Username", required: true
%div
= f.email_field :email, class: "form-control middle", placeholder: "Email", required: true
%div.form-group
= f.label :name
= f.text_field :name, class: "form-control top", required: true, title: "This field is required."
%div.username.form-group
= f.label :username
= 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
= 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
- if current_application_settings.recaptcha_enabled
= recaptcha_tags
%div
= f.submit "Sign up", class: "btn-create btn"
= f.submit "Register", class: "btn-register btn"
.clearfix.prepend-top-20
%p
%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-heading
%h3 Resend unlock email
.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_error_messages!
.clearfix.append-bottom-20
= f.email_field :email, class: 'form-control', placeholder: 'Email', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off'
.form-group.append-bottom-20
= f.label :email
= f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: 'Please provide a valid email address.'
.clearfix
= f.submit 'Resend unlock instructions', class: 'btn btn-success'
......
!!! 5
%html{ lang: "en"}
%html{ lang: "en", class: "devise-layout-html"}
= 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
= render "layouts/header/empty"
= render "layouts/broadcast"
......@@ -9,7 +10,7 @@
.content
= render "layouts/flash"
.row
.col-sm-5.pull-right
.col-sm-5.pull-right.new-session-forms-container
= yield
.col-sm-7.brand-holder.pull-left
%h1
......@@ -28,8 +29,8 @@
- if current_application_settings.sign_in_text.present?
= markdown_field(current_application_settings, :sign_in_text)
%hr
.container
%hr.footer-fixed
.container.footer-container
.footer-links
= link_to "Explore", explore_root_path
= link_to "Help", help_path
......
......@@ -6,7 +6,7 @@
%script#js-authenticate-u2f-setup{ type: "text/template" }
%div
%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" }
%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
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
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(page).to have_content("Please check your email to confirm your account")
......@@ -33,7 +33,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: user.email
fill_in 'new_user_password', with: user.password
click_button "Sign up"
click_button "Register"
expect(current_path).to eq dashboard_projects_path
expect(page).to have_content("Welcome! You have signed up successfully.")
......@@ -52,7 +52,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
click_button "Sign up"
click_button "Register"
expect(current_path).to eq user_registration_path
expect(page).to have_content("error prohibited this user from being saved")
......@@ -69,7 +69,7 @@ feature 'Signup', feature: true do
fill_in 'new_user_username', with: user.username
fill_in 'new_user_email', with: existing_user.email
fill_in 'new_user_password', with: user.password
click_button "Sign up"
click_button "Register"
expect(current_path).to eq user_registration_path
expect(page.body).not_to match(/#{user.password}/)
......
......@@ -160,7 +160,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@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')
click_on "Authenticate via U2F Device"
......@@ -174,7 +174,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user)
@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')
click_on "Authenticate via U2F Device"
......@@ -186,7 +186,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
login_with(user, remember: true)
@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')
within 'div#js-authenticate-u2f' do
......@@ -209,7 +209,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the old U2F device
login_as(current_user)
@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')
click_on "Authenticate via U2F Device"
......@@ -230,7 +230,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
# Try authenticating user with the same U2F device
login_as(current_user)
@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')
click_on "Authenticate via U2F Device"
......@@ -244,7 +244,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
unregistered_device = FakeU2fDevice.new(page, FFaker::Name.first_name)
login_as(user)
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')
click_on "Authenticate via U2F Device"
......@@ -271,7 +271,7 @@ feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', feature:
[first_device, second_device].each do |device|
login_as(user)
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')
click_on "Authenticate via U2F Device"
......
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') }
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Name Surname'
fill_in 'new_user_username', with: 'Great'
fill_in 'new_user_email', with: 'name@mail.com'
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
scenario 'Successful user signin invalidates password reset token' do
......@@ -31,11 +32,12 @@ feature 'Users', feature: true do
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
click_link 'Register'
fill_in 'new_user_name', with: 'Another user name'
fill_in 'new_user_username', with: 'anotheruser'
fill_in 'new_user_email', with: user.email
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(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end
......@@ -51,6 +53,30 @@ feature 'Users', feature: true do
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)
page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
end
......
......@@ -21,7 +21,7 @@
setupButton = this.container.find("#js-login-u2f-device");
setupMessage = this.container.find("p");
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');
inProgressMessage = this.container.find("p");
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