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