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"
|
||||
= button_tag "Sign in", class: "btn-create btn"
|
||||
%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)
|
||||
= 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'
|
||||
|
|
|
|||
|
|
@ -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 signin_enabled? || ldap_enabled? || crowd_enabled?
|
||||
= render 'devise/shared/signin_box'
|
||||
- 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
|
||||
-# Signup only makes sense if you can also sign-in
|
||||
- if signin_enabled? && signup_enabled?
|
||||
= render 'devise/shared/signup_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.
|
||||
- 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'
|
||||
%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"
|
||||
.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,8 +1,9 @@
|
|||
%p
|
||||
%span.light
|
||||
Sign in with
|
||||
- providers = enabled_button_based_providers
|
||||
- providers.each do |provider|
|
||||
%div.login-box
|
||||
%p
|
||||
%span.light
|
||||
- has_icon = provider_has_icon?(provider)
|
||||
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
|
||||
Sign in with
|
||||
- providers = enabled_button_based_providers
|
||||
- providers.each do |provider|
|
||||
%span.light
|
||||
- has_icon = provider_has_icon?(provider)
|
||||
= link_to provider_image_tag(provider), omniauth_authorize_path(:user, provider), method: :post, class: (has_icon ? 'oauth-image-link' : 'btn'), "data-no-turbolink" => "true"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
%p
|
||||
%span.light
|
||||
Already have login and password?
|
||||
%strong
|
||||
= link_to "Sign in", new_session_path(resource_name)
|
||||
|
|
|
|||
|
|
@ -1,32 +1,15 @@
|
|||
.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'
|
||||
- @ldap_servers.each_with_index do |server, i|
|
||||
%div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
|
||||
= render 'devise/sessions/new_ldap', server: server
|
||||
- if signin_enabled?
|
||||
%div#tab-signin.tab-pane
|
||||
= render 'devise/sessions/new_base'
|
||||
- if crowd_enabled?
|
||||
%div.tab-pane.active{id: "tab-crowd"}
|
||||
= render 'devise/sessions/new_crowd'
|
||||
- @ldap_servers.each_with_index do |server, i|
|
||||
%div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
|
||||
= render 'devise/sessions/new_ldap', server: server
|
||||
- if signin_enabled?
|
||||
%div#tab-signin.tab-pane
|
||||
= render 'devise/sessions/new_base'
|
||||
|
||||
- elsif signin_enabled?
|
||||
= render 'devise/sessions/new_base'
|
||||
|
|
|
|||
|
|
@ -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,36 +1,37 @@
|
|||
!!! 5
|
||||
%html{ lang: "en"}
|
||||
%html{ lang: "en", class: "devise-layout-html"}
|
||||
= render "layouts/head"
|
||||
%body.ui_charcoal.login-page.application.navless
|
||||
= Gon::Base.render_data
|
||||
= render "layouts/header/empty"
|
||||
= render "layouts/broadcast"
|
||||
.container.navless-container
|
||||
.content
|
||||
= render "layouts/flash"
|
||||
.row
|
||||
.col-sm-5.pull-right
|
||||
= yield
|
||||
.col-sm-7.brand-holder.pull-left
|
||||
%h1
|
||||
= brand_title
|
||||
- if brand_item
|
||||
= brand_image
|
||||
= brand_text
|
||||
- else
|
||||
%h3 Open source software to collaborate on code
|
||||
%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"
|
||||
.container.navless-container
|
||||
.content
|
||||
= render "layouts/flash"
|
||||
.row
|
||||
.col-sm-5.pull-right.new-session-forms-container
|
||||
= yield
|
||||
.col-sm-7.brand-holder.pull-left
|
||||
%h1
|
||||
= brand_title
|
||||
- if brand_item
|
||||
= brand_image
|
||||
= brand_text
|
||||
- else
|
||||
%h3 Open source software to collaborate on code
|
||||
|
||||
%p
|
||||
Manage git repositories with fine grained access controls that keep your code secure.
|
||||
Perform code reviews and enhance collaboration with merge requests.
|
||||
Each project can also have an issue tracker and a wiki.
|
||||
%p
|
||||
Manage git repositories with fine grained access controls that keep your code secure.
|
||||
Perform code reviews and enhance collaboration with merge requests.
|
||||
Each project can also have an issue tracker and a wiki.
|
||||
|
||||
- if current_application_settings.sign_in_text.present?
|
||||
= markdown_field(current_application_settings, :sign_in_text)
|
||||
|
||||
%hr
|
||||
.container
|
||||
.footer-links
|
||||
= link_to "Explore", explore_root_path
|
||||
= link_to "Help", help_path
|
||||
= link_to "About GitLab", "https://about.gitlab.com/"
|
||||
%hr.footer-fixed
|
||||
.container.footer-container
|
||||
.footer-links
|
||||
= link_to "Explore", explore_root_path
|
||||
= link_to "Help", help_path
|
||||
= link_to "About GitLab", "https://about.gitlab.com/"
|
||||
|
|
|
|||
|
|
@ -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