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.
......
......@@ -83,6 +83,798 @@ Rails.application.routes.draw do
draw :group
draw :user
draw :project
#
# Import
#
namespace :import do
resource :github, only: [:create, :new], controller: :github do
post :personal_access_token
get :status
get :callback
get :jobs
end
resource :gitlab, only: [:create], controller: :gitlab do
get :status
get :callback
get :jobs
end
resource :bitbucket, only: [:create], controller: :bitbucket do
get :status
get :callback
get :jobs
end
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :fogbugz, only: [:create, :new], controller: :fogbugz do
get :status
post :callback
get :jobs
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
resource :gitlab_project, only: [:create, :new] do
post :create
end
end
#
# Uploads
#
scope path: :uploads do
# Note attachments and User/Group/Project avatars
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
# Appearance
get ":model/:mounted_as/:id/:filename",
to: "uploads#show",
constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
# Project markdown uploads
get ":namespace_id/:project_id/:secret/:filename",
to: "projects/uploads#show",
constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
end
# Redirect old note attachments path to new uploads path.
get "files/note/:id/:filename",
to: redirect("uploads/note/attachment/%{id}/%{filename}"),
constraints: { filename: /[^\/]+/ }
#
# Explore area
#
namespace :explore do
resources :projects, only: [:index] do
collection do
get :trending
get :starred
end
end
resources :groups, only: [:index]
resources :snippets, only: [:index]
root to: 'projects#trending'
end
# Compatibility with old routing
get 'public' => 'explore/projects#index'
get 'public/projects' => 'explore/projects#index'
#
# Admin Area
#
namespace :admin do
resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
resources :keys, only: [:show, :destroy]
resources :identities, except: [:show]
member do
get :projects
get :keys
get :groups
put :block
put :unblock
put :unlock
put :confirm
post :impersonate
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
end
resource :impersonation, only: :destroy
resources :abuse_reports, only: [:index, :destroy]
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham
end
end
resources :applications
resources :groups, constraints: { id: /[^\/]+/ } do
member do
put :members_update
end
end
resources :deploy_keys, only: [:index, :new, :create, :destroy]
resources :hooks, only: [:index, :create, :destroy] do
get :test
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
post :preview, on: :collection
end
resource :logs, only: [:show]
resource :health_check, controller: 'health_check', only: [:show]
resource :background_jobs, controller: 'background_jobs', only: [:show]
resource :system_info, controller: 'system_info', only: [:show]
resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
root to: 'projects#index', as: :projects
resources(:projects,
path: '/',
constraints: { id: /[a-zA-Z.0-9_\-]+/ },
only: [:index, :show]) do
root to: 'projects#show'
member do
put :transfer
post :repository_check
end
resources :runner_projects, only: [:create, :destroy]
end
end
resource :appearances, only: [:show, :create, :update], path: 'appearance' do
member do
get :preview
delete :logo
delete :header_logos
end
end
resource :application_settings, only: [:show, :update] do
resources :services, only: [:index, :edit, :update]
put :reset_runners_token
put :reset_health_check_token
put :clear_repository_check_states
end
resources :labels
resources :runners, only: [:index, :show, :update, :destroy] do
member do
get :resume
get :pause
end
end
resources :builds, only: :index do
collection do
post :cancel_all
end
end
root to: 'dashboard#index'
end
#
# Profile Area
#
resource :profile, only: [:show, :update] do
member do
get :audit_log
get :applications, to: 'oauth/applications#index'
put :reset_private_token
put :update_username
end
scope module: :profiles do
resource :account, only: [:show] do
member do
delete :unlink
end
end
resource :notifications, only: [:show, :update]
resource :password, only: [:new, :create, :edit, :update] do
member do
put :reset
end
end
resource :preferences, only: [:show, :update]
resources :keys, only: [:index, :show, :new, :create, :destroy]
resources :emails, only: [:index, :create, :destroy]
resource :avatar, only: [:destroy]
resources :personal_access_tokens, only: [:index, :create] do
member do
put :revoke
end
end
resource :two_factor_auth, only: [:show, :create, :destroy] do
member do
post :create_u2f
post :codes
patch :skip
end
end
resources :u2f_registrations, only: [:destroy]
end
end
scope(path: 'u/:username',
as: :user,
constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
controller: :users) do
get :calendar
get :calendar_activities
get :groups
get :projects
get :contributed, as: :contributed_projects
get :snippets
get :exists
get '/', action: :show
end
#
# Dashboard Area
#
resource :dashboard, controller: 'dashboard', only: [] do
get :issues
get :merge_requests
get :activity
scope module: :dashboard do
resources :milestones, only: [:index, :show]
resources :labels, only: [:index]
resources :groups, only: [:index]
resources :snippets, only: [:index]
resources :todos, only: [:index, :destroy] do
collection do
delete :destroy_all
end
end
resources :projects, only: [:index] do
collection do
get :starred
end
end
end
root to: "dashboard/projects#index"
end
#
# Groups Area
#
resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
member do
get :issues
get :merge_requests
get :projects
get :activity
end
scope module: :groups do
resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
end
end
resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
registrations: :registrations,
passwords: :passwords,
sessions: :sessions,
confirmations: :confirmations }
devise_scope :user do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
get '/users/almost_there' => 'confirmations#almost_there'
end
root to: "root#index"
#
# Project Area
#
resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
[:new, :create, :index], path: "/") do
member do
put :transfer
delete :remove_fork
post :archive
post :unarchive
post :housekeeping
post :toggle_star
post :preview_markdown
post :export
post :remove_export
post :generate_new_export
get :download_export
get :autocomplete_sources
get :activity
get :refs
end
scope module: :projects do
scope constraints: { id: /.+\.git/, format: nil } do
# Git HTTP clients ('git clone' etc.)
get '/info/refs', to: 'git_http#info_refs'
post '/git-upload-pack', to: 'git_http#git_upload_pack'
post '/git-receive-pack', to: 'git_http#git_receive_pack'
# Git LFS API (metadata)
post '/info/lfs/objects/batch', to: 'lfs_api#batch'
post '/info/lfs/objects', to: 'lfs_api#deprecated'
get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
# GitLab LFS object storage
scope constraints: { oid: /[a-f0-9]{64}/ } do
get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
scope constraints: { size: /[0-9]+/ } do
put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
end
end
end
# Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else.
#
git_http_handshake = lambda do |request|
request.query_string.blank? ||
request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
end
ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank?
path
end
get '/info/refs', constraints: git_http_handshake, to: ref_redirect
# Blob routes:
get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
#
# Templates
#
get '/templates/:template_type/:key' => 'templates#show', as: :template
scope do
get(
'/blob/*id/diff',
to: 'blob#diff',
constraints: { id: /.+/, format: false },
as: :blob_diff
)
get(
'/blob/*id',
to: 'blob#show',
constraints: { id: /.+/, format: false },
as: :blob
)
delete(
'/blob/*id',
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
put(
'/blob/*id',
to: 'blob#update',
constraints: { id: /.+/, format: false }
)
post(
'/blob/*id',
to: 'blob#create',
constraints: { id: /.+/, format: false }
)
end
scope do
get(
'/raw/*id',
to: 'raw#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :raw
)
end
scope do
get(
'/tree/*id',
to: 'tree#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :tree
)
end
scope do
get(
'/find_file/*id',
to: 'find_file#show',
constraints: { id: /.+/, format: /html/ },
as: :find_file
)
end
scope do
get(
'/files/*id',
to: 'find_file#list',
constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
as: :files
)
end
scope do
post(
'/create_dir/*id',
to: 'tree#create_dir',
constraints: { id: /.+/ },
as: 'create_dir'
)
end
scope do
get(
'/blame/*id',
to: 'blame#show',
constraints: { id: /.+/, format: /(html|js)/ },
as: :blame
)
end
scope do
get(
'/commits/*id',
to: 'commits#show',
constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
as: :commits
)
end
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
member do
get :branches
get :builds
get :pipelines
post :cancel_builds
post :retry_builds
post :revert
post :cherry_pick
get :diff_for_path
end
end
resources :compare, only: [:index, :create] do
collection do
get :diff_for_path
end
end
get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
# Don't use format parameter as file extension (old 3.0.x behavior)
# See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
scope format: false do
resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
member do
get :commits
get :ci
get :languages
end
end
end
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get 'raw'
end
end
WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
scope do
# Order matters to give priority to these matches
get '/wikis/git_access', to: 'wikis#git_access'
get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
post '/wikis', to: 'wikis#create'
get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
end
resource :repository, only: [:create] do
member do
get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
end
end
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
get :test
end
end
resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
member do
put :enable
put :disable
end
end
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
resources :refs, only: [] do
collection do
get 'switch'
end
member do
# tree viewer logs
get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
# Directories with leading dots erroneously get rejected if git
# ref regex used in constraints. Regex verification now done in controller.
get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
id: /.*/,
path: /.*/
}
end
end
resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
member do
get :commits
get :diffs
get :conflicts
get :builds
get :pipelines
get :merge_check
post :merge
post :cancel_merge_when_build_succeeds
get :ci_status
post :toggle_subscription
post :remove_wip
get :diff_for_path
post :resolve_conflicts
end
collection do
get :branch_from
get :branch_to
get :update_branches
get :diff_for_path
post :bulk_update
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
member do
post :resolve
delete :resolve, action: :unresolve
end
end
end
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
resource :release, only: [:edit, :update]
end
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
resources :pipelines, only: [:index, :new, :create, :show] do
collection do
resource :pipelines_settings, path: 'settings', only: [:show, :update]
end
member do
post :cancel
post :retry
end
end
resources :environments
resource :cycle_analytics, only: [:show]
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
resources :artifacts, only: [] do
collection do
get :latest_succeeded,
path: '*ref_name_and_path',
format: false
end
end
end
member do
get :status
post :cancel
post :retry
post :play
post :erase
get :trace
get :raw
end
resource :artifacts, only: [] do
get :download
get :browse, path: 'browse(/*path)', format: false
get :file, path: 'file/*path', format: false
post :keep
end
end
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
end
resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
resources :milestones, constraints: { id: /\d+/ } do
member do
put :sort_issues
put :sort_merge_requests
end
end
resources :labels, except: [:show], constraints: { id: /\d+/ } do
collection do
post :generate
post :set_priorities
end
member do
post :toggle_subscription
delete :remove_priority
end
end
resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
member do
post :toggle_subscription
post :mark_as_spam
get :referenced_merge_requests
get :related_branches
get :can_create_branch
end
collection do
post :bulk_update
end
end
resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
collection do
delete :leave
# Used for import team
# from another project
get :import
post :apply_import
end
member do
post :resend_invite
end
end
resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
member do
delete :delete_attachment
post :resolve
delete :resolve, action: :unresolve
end
end
resource :board, only: [:show] do
scope module: :boards do
resources :issues, only: [:update]
resources :lists, only: [:index, :create, :update, :destroy] do
collection do
post :generate
end
resources :issues, only: [:index]
end
end
end
resources :todos, only: [:create]
resources :uploads, only: [:create] do
collection do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
resources :runners, only: [:index, :edit, :update, :destroy, :show] do
member do
get :resume
get :pause
end
collection do
post :toggle_shared_runners
end
end
resources :runner_projects, only: [:create, :destroy]
resources :badges, only: [:index] do
collection do
scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
constraints format: /svg/ do
get :build
get :coverage
end
end
end
end
end
end
end
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
......
......@@ -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