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
This commit is contained in:
		
							parent
							
								
									602cac526d
								
							
						
					
					
						commit
						1dd826d4aa
					
				|  | @ -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; | ||||
| 
 | ||||
|   })(); | ||||
|  |  | |||
|  | @ -0,0 +1,100 @@ | |||
| ((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 = {})); | ||||
|  | @ -0,0 +1,131 @@ | |||
| ((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; | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -1,6 +1,6 @@ | |||
| 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! | ||||
|  |  | |||
|  | @ -1,9 +1,12 @@ | |||
| - 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" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,14 +1,14 @@ | |||
| = 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' | ||||
|  |  | |||
|  | @ -1,19 +1,21 @@ | |||
| = 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) | ||||
|     %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' | ||||
|  |  | |||
|  | @ -1,12 +1,12 @@ | |||
| = 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" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| = 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? | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| = 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"} | ||||
|  |  | |||
|  | @ -1,6 +1,10 @@ | |||
| = 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"} | ||||
|  |  | |||
|  | @ -1,19 +1,23 @@ | |||
| - 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 } | ||||
|  |  | |||
|  | @ -1,3 +1,4 @@ | |||
| %div.login-box | ||||
|   %p | ||||
|     %span.light | ||||
|       Sign in with   | ||||
|  |  | |||
|  | @ -1,5 +1,4 @@ | |||
| %p | ||||
|   %span.light | ||||
|     Already have login and password? | ||||
|   %strong | ||||
|     = link_to "Sign in", new_session_path(resource_name) | ||||
|  |  | |||
|  | @ -1,23 +1,6 @@ | |||
| .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' | ||||
|  |  | |||
|  | @ -1,28 +1,30 @@ | |||
| .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? | ||||
|  |  | |||
|  | @ -0,0 +1,4 @@ | |||
| //  = 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 | ||||
|  | @ -0,0 +1,10 @@ | |||
| %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' | ||||
|  | @ -0,0 +1,5 @@ | |||
| %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 | ||||
|  | @ -1,12 +1,12 @@ | |||
| = 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' | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,7 +1,8 @@ | |||
| !!! 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. | ||||
|  |  | |||
							
								
								
									
										792
									
								
								config/routes.rb
								
								
								
								
							
							
						
						
									
										792
									
								
								config/routes.rb
								
								
								
								
							|  | @ -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" | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,15 +1,16 @@ | |||
| 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"); | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue