(web) addressing config changes - login page p1

This commit is contained in:
Alexey Kontsevoy 2017-02-13 17:13:22 -05:00
parent 652a7d302e
commit 72fb5cdea3
15 changed files with 624 additions and 265 deletions

View file

@ -15,8 +15,8 @@ limitations under the License.
*/
module.exports.App = require('./app.jsx');
module.exports.Login = require('./login.jsx');
module.exports.NewUser = require('./newUser.jsx');
module.exports.Login = require('./user/login.jsx');
module.exports.NewUser = require('./user/newUser.jsx');
module.exports.Nodes = require('./nodes/main.jsx');
module.exports.Sessions = require('./sessions/main.jsx');
module.exports.CurrentSessionHost = require('./currentSession/main.jsx');

View file

@ -1,147 +0,0 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
var $ = require('jQuery');
var reactor = require('app/reactor');
var LinkedStateMixin = require('react-addons-linked-state-mixin');
var {actions, getters} = require('app/modules/user');
var GoogleAuthInfo = require('./googleAuthLogo');
var cfg = require('app/config');
var {TeleportLogo} = require('./icons.jsx');
var {SECOND_FACTOR_TYPE_HOTP, SECOND_FACTOR_TYPE_OIDC, SECOND_FACTOR_TYPE_U2F} = require('app/services/auth');
var LoginInputForm = React.createClass({
mixins: [LinkedStateMixin],
getInitialState() {
return {
user: '',
password: '',
token: '',
provider: null,
secondFactorType: SECOND_FACTOR_TYPE_HOTP
}
},
onLogin(e){
e.preventDefault();
this.state.secondFactorType = SECOND_FACTOR_TYPE_HOTP;
// token field is required for Google Authenticator
$('input[name=token]').addClass("required");
if (this.isValid()) {
this.props.onClick(this.state);
}
},
providerLogin: function (provider) {
var self = this;
return function (e) {
e.preventDefault();
self.state.secondFactorType = SECOND_FACTOR_TYPE_OIDC;
self.state.provider = provider.id;
self.props.onClick(self.state);
}
},
onLoginWithU2f: function(e) {
e.preventDefault();
this.state.secondFactorType = SECOND_FACTOR_TYPE_U2F;
// token field not required for U2F
$('input[name=token]').removeClass("required");
if (this.isValid()) {
this.props.onClick(this.state);
}
},
isValid: function() {
var $form = $(this.refs.form);
return $form.length === 0 || $form.valid();
},
render() {
let {isProcessing, isFailed, message } = this.props.attemp;
let providers = cfg.getAuthProviders();
let useU2f = !!cfg.getU2fAppId();
return (
<form ref="form" className="grv-login-input-form">
<h3> Welcome to Teleport </h3>
<div className="">
<div className="form-group">
<input autoFocus valueLink={this.linkState('user')} className="form-control required" placeholder="User name" name="userName" />
</div>
<div className="form-group">
<input valueLink={this.linkState('password')} type="password" name="password" className="form-control required" placeholder="Password"/>
</div>
<div className="form-group">
<input autoComplete="off" valueLink={this.linkState('token')} className="form-control required" name="token" placeholder="Two factor token (Google Authenticator)"/>
</div>
<button onClick={this.onLogin} disabled={isProcessing} type="submit" className="btn btn-primary block full-width m-b">Login</button>
{ useU2f ? <button onClick={this.onLoginWithU2f} disabled={isProcessing} type="submit" className="btn btn-primary block full-width m-b">Login with U2F</button> : null }
{ providers.map((provider) => <button onClick={this.providerLogin(provider)} type="submit" className="btn btn-danger block full-width m-b">With {provider.display}</button>) }
{ isProcessing && this.state.secondFactorType == SECOND_FACTOR_TYPE_U2F ? (<label className="help-block">Insert your U2F key and press the button on the key</label>) : null }
{ isFailed ? (<label className="error">{message}</label>) : null }
</div>
</form>
);
}
})
var Login = React.createClass({
mixins: [reactor.ReactMixin],
getDataBindings() {
return {
attemp: getters.loginAttemp
}
},
onClick(inputData){
var loc = this.props.location;
var redirect = cfg.routes.app;
if(loc.state && loc.state.redirectTo){
redirect = loc.state.redirectTo;
}
actions.login(inputData, redirect);
},
render() {
return (
<div className="grv-login text-center">
<TeleportLogo/>
<div className="grv-content grv-flex">
<div className="grv-flex-column">
<LoginInputForm attemp={this.state.attemp} onClick={this.onClick}/>
<GoogleAuthInfo/>
<div className="grv-login-info">
<i className="fa fa-question"></i>
<strong>New Account or forgot password?</strong>
<div>Ask for assistance from your Company administrator</div>
</div>
</div>
</div>
</div>
);
}
});
module.exports = Login;

View file

@ -14,18 +14,20 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var React = require('react');
import React from 'react';
var GoogleAuthInfo = React.createClass({
render() {
return (
<div className="grv-google-auth text-left">
<div className="grv-icon-google-auth"/>
<strong>Google Authenticator</strong>
<div>Download <a href="https://support.google.com/accounts/answer/1066447?hl=en">Google Authenticator</a> on your phone to access your two factor token</div>
</div>
);
}
})
const GoogleAuthInfo = () => {
return (
<div className="grv-google-auth text-left">
<div className="grv-icon-google-auth"/>
<strong>Google Authenticator</strong>
<div>Download
<a href="https://support.google.com/accounts/answer/1066447?hl=en">
<span> Google Authenticator </span>
</a>
on your phone to access your two factor token</div>
</div>
);
}
module.exports = GoogleAuthInfo;
export default GoogleAuthInfo;

View file

@ -0,0 +1,287 @@
/*
Copyright 2015 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import $ from 'jQuery';
import reactor from 'app/reactor';
import {actions, getters} from 'app/modules/user';
import GoogleAuthInfo from './googleAuthLogo';
import cfg from 'app/config';
import { TeleportLogo } from './../icons.jsx';
import { SsoBtnList } from './ssoBtnList';
import { Auth2faType, AuthType } from 'app/services/enums';
const Login = React.createClass({
mixins: [reactor.ReactMixin],
getDataBindings() {
return {
attemp: getters.loginAttemp
}
},
onLoginWithOidc(providerName){
let redirect = this.getRedirectUrl();
actions.loginWithOidc(providerName, redirect);
},
onLoginWithU2f(username, password) {
let redirect = this.getRedirectUrl();
actions.loginWithU2f(username, password, redirect);
},
onLogin(username, password, token) {
let redirect = this.getRedirectUrl();
actions.login(username, password, token, redirect);
},
getRedirectUrl() {
let loc = this.props.location;
let redirect = cfg.routes.app;
if (loc.state && loc.state.redirectTo) {
redirect = loc.state.redirectTo;
}
return redirect;
},
render() {
let {attemp} = this.state;
let provider = cfg.getAuthProvider();
let authType = cfg.getAuthType();
let auth2faType = cfg.getAuth2faType();
return (
<div className="grv-login text-center">
<TeleportLogo/>
<div className="grv-content grv-flex">
<div className="grv-flex-column">
<LoginInputForm
authProvider={provider}
auth2faType={auth2faType}
authType={authType}
onLoginWithOidc={this.onLoginWithOidc}
onLoginWithU2f={this.onLoginWithU2f}
onLogin={this.onLogin}
attemp={attemp}
/>
<LoginFooter auth2faType={auth2faType}/>
</div>
</div>
</div>
);
}
});
const LoginInputForm = React.createClass({
getInitialState() {
return {
user: '',
password: '',
token: ''
}
},
onLogin(e) {
e.preventDefault();
this.state.secondFactorType = Auth2faType.OTP;
if (this.isValid()) {
let { user, password, token } = this.state;
this.props.onLogin(user, password, token);
}
},
onLoginWithU2f(e) {
e.preventDefault();
if (this.isValid()) {
let { user, password } = this.state;
this.props.onLoginWithU2f(user, password);
}
},
onLoginWithOidc(e) {
e.preventDefault();
this.props.onLoginWithOidc(this.props.authProvider);
},
isValid() {
var $form = $(this.refs.form);
return $form.length === 0 || $form.valid();
},
needsCredentials() {
return this.props.authType === AuthType.LOCAL || this.needs2fa();
},
needs2fa() {
return !!this.props.auth2faType && this.props.auth2faType !== Auth2faType.DISABLED;
},
render2faFields() {
if (!this.needs2fa() || this.props.auth2faType !== Auth2faType.OTP) {
return null;
}
return (
<div className="form-group">
<input
autoComplete="off"
value={this.state.token}
onChange={e => this.onChangeState('token', e.target.value)}
className="form-control required"
name="token"
placeholder="Two factor token (Google Authenticator)"/>
</div>
)
},
onChangeState(propName, value) {
this.setState({
[propName]: value
});
},
renderNameAndPassFields() {
if (!this.needsCredentials()) {
return null;
}
return (
<div>
<div className="form-group">
<input
autoFocus
value={this.state.user}
onChange={e => this.onChangeState('user', e.target.value)}
className="form-control required"
placeholder="User name"
name="userName"/>
</div>
<div className="form-group">
<input
value={this.state.password}
onChange={e => this.onChangeState('password', e.target.value)}
type="password"
name="password"
className="form-control required"
placeholder="Password"/>
</div>
</div>
)
},
renderLoginBtn() {
let { isProcessing } = this.props.attemp;
if (!this.needsCredentials()) {
return null;
}
let $helpBlock = isProcessing && this.props.auth2faType === Auth2faType.UTF ? (
<div className="help-block">
Insert your U2F key and press the button on the key
</div>
) : null;
let onClick = this.props.auth2faType === Auth2faType.UTF ?
this.onLoginWithU2f : this.onLogin;
return (
<div>
<button
onClick={onClick}
disabled={isProcessing}
type="submit"
className="btn btn-primary block full-width m-b">
Login
</button>
{$helpBlock}
</div>
);
},
renderSsoBtns() {
let { authType, authProvider, attemp } = this.props;
if (authType !== AuthType.OIDC) {
return null;
}
let ssoProvider = {
name: authProvider,
displayName: authProvider
}
return (
<SsoBtnList
prefixText="Login with "
isDisabled={attemp.isProcessing}
providers={[ssoProvider]}
onClick={this.onLoginWithOidc} />
)
},
render() {
let { isFailed, message } = this.props.attemp;
let $error = isFailed ? (
<label className="error">{message}</label>
) : null;
return (
<div>
<form ref="form" className="grv-login-input-form">
<h3> Welcome to Teleport </h3>
<div>
{this.renderNameAndPassFields()}
{this.render2faFields()}
{this.renderLoginBtn()}
{this.renderSsoBtns()}
{$error}
</div>
</form>
</div>
);
}
})
LoginInputForm.propTypes = {
authProvider: React.PropTypes.string,
auth2faType: React.PropTypes.string,
authType: React.PropTypes.string,
onLoginWithOidc: React.PropTypes.func.isRequired,
onLoginWithU2f: React.PropTypes.func.isRequired,
onLogin: React.PropTypes.func.isRequired,
attemp: React.PropTypes.object.isRequired
}
const LoginFooter = ({auth2faType}) => {
let $googleHint = auth2faType === Auth2faType.OTP ? <GoogleAuthInfo /> : null;
return (
<div>
{$googleHint}
<div className="grv-login-info">
<i className="fa fa-question"></i>
<strong>New Account or forgot password?</strong>
<div>Ask for assistance from your Company administrator</div>
</div>
</div>
)
}
export default Login;

View file

@ -20,8 +20,8 @@ var reactor = require('app/reactor');
var {actions, getters} = require('app/modules/user');
var LinkedStateMixin = require('react-addons-linked-state-mixin');
var GoogleAuthInfo = require('./googleAuthLogo');
var {ErrorPage, ErrorTypes} = require('./msgPage');
var {TeleportLogo} = require('./icons.jsx');
var {ErrorPage, ErrorTypes} = require('./../msgPage');
var {TeleportLogo} = require('./../icons.jsx');
var {SECOND_FACTOR_TYPE_HOTP, SECOND_FACTOR_TYPE_U2F} = require('app/services/auth');
var cfg = require('app/config');

View file

@ -0,0 +1,63 @@
import React from 'react';
import classnames from 'classnames';
import { AuthProviderEnum } from 'app/services/enums';
const ProviderIcon = ({ type }) => {
let iconClass = classnames('fa', {
'fa-google': type === AuthProviderEnum.GOOGLE,
'fa-windows': type === AuthProviderEnum.MS,
'fa-github': type === AuthProviderEnum.GITHUB,
'fa-bitbucket': type === AuthProviderEnum.BITBUCKET
});
if (iconClass === 'fa') {
iconClass = `${iconClass} fa-openid`;
}
return (
<div className="--sso-icon">
<span className={iconClass}></span>
</div>
)
}
const getProviderBtnClass = type => {
switch (type) {
case AuthProviderEnum.BITBUCKET:
return 'btn-bitbucket';
case AuthProviderEnum.GITHUB:
return 'btn-github';
case AuthProviderEnum.MS:
return 'btn-microsoft';
case AuthProviderEnum.GOOGLE:
return 'btn-google';
default:
return 'btn-openid';
}
}
const SsoBtnList = ({providers, prefixText, isDisabled, onClick}) => {
let $btns = providers.map((item, index) => {
let { name, displayName } = item;
displayName = displayName || name;
let title = `${prefixText} ${displayName}`
let providerBtnClass = getProviderBtnClass(name);
let btnClass = `btn grv-user-btn-sso full-width ${providerBtnClass}`;
return (
<button key={index}
disabled={isDisabled}
className={btnClass}
onClick={onClick}>
<ProviderIcon type={name}/>
<span>{title}</span>
</button>
)
})
return (
<div> {$btns} </div>
)
}
export { SsoBtnList }

View file

@ -115,6 +115,18 @@ let cfg = {
return cfg.auth.oidc_connectors;
},
getAuthProvider() {
return 'github';
},
getAuthType() {
return 'oidc';
},
getAuth2faType() {
return 'utf'
},
getU2fAppId(){
return cfg.auth.u2f_appid;
},

View file

@ -22,7 +22,7 @@ var auth = require('app/services/auth');
var session = require('app/services/session');
var cfg = require('app/config');
var api = require('app/services/api');
var {SECOND_FACTOR_TYPE_OIDC, SECOND_FACTOR_TYPE_U2F} = require('app/services/auth');
var { SECOND_FACTOR_TYPE_U2F} = require('app/services/auth');
var actions = {
@ -79,31 +79,33 @@ var actions = {
},
login({user, password, token, provider, secondFactorType}, redirect){
if(secondFactorType == SECOND_FACTOR_TYPE_OIDC){
let fullPath = cfg.getFullUrl(redirect);
window.location = cfg.api.getSsoUrl(fullPath, provider);
return;
}
loginWithOidc(provider, redirect) {
let fullPath = cfg.getFullUrl(redirect);
window.location = cfg.api.getSsoUrl(fullPath, provider);
},
loginWithU2f(user, password, redirect) {
let promise = auth.u2fLogin(user, password);
this._handleLoginPromise(promise, redirect);
},
login(user, password, token, redirect) {
let promise = auth.login(user, password, token);
this._handleLoginPromise(promise, redirect);
},
_handleLoginPromise(promise, redirect) {
restApiActions.start(TRYING_TO_LOGIN);
var onSuccess = function(sessionData){
restApiActions.success(TRYING_TO_LOGIN);
reactor.dispatch(TLPT_RECEIVE_USER, sessionData.user);
session.getHistory().push({pathname: redirect});
};
var onFailure = function(err){
var msg = err.responseJSON ? err.responseJSON.message : 'Error';
restApiActions.fail(TRYING_TO_LOGIN, msg);
};
if(secondFactorType == SECOND_FACTOR_TYPE_U2F){
auth.u2fLogin(user, password).done(onSuccess).fail(onFailure);
} else {
auth.login(user, password, token).done(onSuccess).fail(onFailure);
}
promise
.done(sessionData => {
restApiActions.success(TRYING_TO_LOGIN);
reactor.dispatch(TLPT_RECEIVE_USER, sessionData.user);
session.getHistory().push({pathname: redirect});
})
.fail(err => {
let msg = api.getErrorText(err);
restApiActions.fail(TRYING_TO_LOGIN, msg);
})
}
}

View file

@ -14,8 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var $ = require("jQuery");
var session = require('./session');
import $ from 'jQuery';
import session from './session';
const api = {
@ -51,7 +51,25 @@ const api = {
}
return $.ajax($.extend({}, defaultCfg, cfg));
}
},
getErrorText(err){
let msg = 'Unknown error';
if (err instanceof Error) {
return err.message || msg;
}
if(err.responseJSON && err.responseJSON.message){
return err.responseJSON.message;
}
if (err.responseJSON && err.responseJSON.error) {
return err.responseJSON.error.message || msg;
}
return msg;
}
}
module.exports = api;
export default api;

View file

@ -14,19 +14,18 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
var api = require('./api');
var session = require('./session');
var cfg = require('app/config');
var $ = require('jQuery');
var logger = require('app/common/logger').create('services/auth');
import api from './api';
import session from './session';
import cfg from 'app/config';
import $ from 'jQuery';
import Logger from 'app/common/logger';
require('u2f-api-polyfill'); // This puts it in window.u2f
// This puts it in window.u2f
import 'u2f-api-polyfill';
const logger = Logger.create('services/auth');
const AUTH_IS_RENEWING = 'GRV_AUTH_IS_RENEWING';
const PROVIDER_GOOGLE = 'google';
const SECOND_FACTOR_TYPE_HOTP = 'hotp';
const SECOND_FACTOR_TYPE_OIDC = 'oidc';
const SECOND_FACTOR_TYPE_U2F = 'u2f';
const CHECK_TOKEN_REFRESH_RATE = 10 * 1000; // 10 sec
@ -225,11 +224,7 @@ const auth = {
}
return {responseJSON:{message:"U2F Error: " + errorMsg}};
}
}
module.exports = auth;
module.exports.PROVIDER_GOOGLE = PROVIDER_GOOGLE;
module.exports.SECOND_FACTOR_TYPE_HOTP = SECOND_FACTOR_TYPE_HOTP;
module.exports.SECOND_FACTOR_TYPE_OIDC = SECOND_FACTOR_TYPE_OIDC;
module.exports.SECOND_FACTOR_TYPE_U2F = SECOND_FACTOR_TYPE_U2F;
export default auth;

View file

@ -1,6 +1,29 @@
module.exports = {
export default {
EventTypeEnum: {
START: 'session.start',
END: 'session.end'
}
},
AuthType: {
LOCAL: 'local',
OIDC: 'oidc',
LDAP: 'ldap'
},
Auth2faType: {
UTF: 'utf',
OTP: 'otp',
DISABLED: 'off'
},
AuthProviderEnum: {
GOOGLE: 'google',
MS: 'microsoft',
GITHUB: 'github',
BITBUCKET: 'bitbucket'
}
}

View file

@ -0,0 +1,115 @@
/*
* Social Buttons for Bootstrap (4-12-0)
*
* Copyright 2013-2015 Panayiotis Lipiridis
* Licensed under the MIT License
*
* https://github.com/lipis/bootstrap-social
*/
$bs-height-base: ($line-height-computed + $padding-base-vertical * 2);
$bs-height-lg: (floor($font-size-large * $line-height-base) + $padding-large-vertical * 2);
$bs-height-sm: (floor($font-size-small * 1.5) + $padding-small-vertical * 2);
$bs-height-xs: (floor($font-size-small * 1.2) + $padding-small-vertical + 1);
.btn-social {
position: relative;
padding-left: ($bs-height-base + $padding-base-horizontal);
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
> :first-child {
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: $bs-height-base;
line-height: ($bs-height-base + 2);
font-size: 1.6em;
text-align: center;
border-right: 1px solid rgba(0, 0, 0, 0.2);
}
&.btn-lg {
padding-left: ($bs-height-lg + $padding-large-horizontal);
> :first-child {
line-height: $bs-height-lg;
width: $bs-height-lg;
font-size: 1.8em;
}
}
&.btn-sm {
padding-left: ($bs-height-sm + $padding-small-horizontal);
> :first-child {
line-height: $bs-height-sm;
width: $bs-height-sm;
font-size: 1.4em;
}
}
&.btn-xs {
padding-left: ($bs-height-xs + $padding-small-horizontal);
> :first-child {
line-height: $bs-height-xs;
width: $bs-height-xs;
font-size: 1.2em;
}
}
}
.btn-social-icon {
@extend .btn-social;
height: ($bs-height-base + 2);
width: ($bs-height-base + 2);
padding: 0;
> :first-child {
border: none;
text-align: center;
width: 100%!important;
}
&.btn-lg {
height: $bs-height-lg;
width: $bs-height-lg;
padding-left: 0;
padding-right: 0;
}
&.btn-sm {
height: ($bs-height-sm + 2);
width: ($bs-height-sm + 2);
padding-left: 0;
padding-right: 0;
}
&.btn-xs {
height: ($bs-height-xs + 2);
width: ($bs-height-xs + 2);
padding-left: 0;
padding-right: 0;
}
}
@mixin btn-social($color-bg, $color: #fff) {
background-color: $color-bg;
@include button-variant($color, $color-bg, rgba(0,0,0,.2));
}
.btn-adn { @include btn-social(#d87a68); }
.btn-bitbucket { @include btn-social(#205081); }
.btn-dropbox { @include btn-social(#1087dd); }
.btn-facebook { @include btn-social(#3b5998); }
.btn-flickr { @include btn-social(#ff0084); }
.btn-foursquare { @include btn-social(#f94877); }
.btn-github { @include btn-social(#444444); }
.btn-google { @include btn-social(#dd4b39); }
.btn-instagram { @include btn-social(#3f729b); }
.btn-linkedin { @include btn-social(#007bb6); }
.btn-microsoft { @include btn-social(#2672ec); }
.btn-odnoklassniki { @include btn-social(#f4731c); }
.btn-openid { @include btn-social(#f7931e); }
.btn-pinterest { @include btn-social(#cb2027); }
.btn-reddit { @include btn-social(#eff7ff, #000); }
.btn-soundcloud { @include btn-social(#ff5500); }
.btn-tumblr { @include btn-social(#2c4762); }
.btn-twitter { @include btn-social(#55acee); }
.btn-vimeo { @include btn-social(#1ab7ea); }
.btn-vk { @include btn-social(#587ea3); }
.btn-yahoo { @include btn-social(#720e9e); }

View file

@ -58,46 +58,24 @@ limitations under the License.
}
}
.rotating {
animation: rotating-function 4.00s linear infinite;
}
@-webkit-keyframes rotating-function {
from {
-webkit-transform: rotate(0deg);
.grv-user-btn-sso{
&:not(:first-child){
margin-top: 15px;
}
to {
-webkit-transform: rotate(360deg);
}
}
@-moz-keyframes rotating-function {
from {
-moz-transform: rotate(0deg);
}
to {
-moz-transform: rotate(360deg);
}
}
@-ms-keyframes rotating-function {
from {
-ms-transform: rotate(0deg);
}
to {
-ms-transform: rotate(360deg);
}
}
@-o-keyframes rotating-function {
from {
-o-transform: rotate(0deg);
}
to {
-o-transform: rotate(360deg);
}
}
@keyframes rotating-function {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
max-width: 400px;
text-align: center;
margin: 0 auto;
position: relative;
.--sso-icon{
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 32px;
font-size: 1.6em;
text-align: center;
border-right: 1px solid rgba(0, 0, 0, 0.2);
}
}

View file

@ -20,6 +20,7 @@ limitations under the License.
@import "~assets/css/toastr-2.1.2.css";
@import "~perfect-scrollbar/dist/css/perfect-scrollbar.css";
@import "bootstrap/bootstrap";
@import "bootstrap/bootstrap-social";
@import 'inspinia/style';
@import "grv-invite";

View file

@ -15,12 +15,20 @@ limitations under the License.
*/
var baseCfg = require('./webpack.base');
var config = require('./webpack.config');
config.devtool = 'source-map';
config.output.filename = '[name].js';
config.cache = true;
config.module = {
var output = Object.assign({}, baseCfg.output, {
filename: '[name].js'
});
var cfg = {
entry: baseCfg.entry,
resolve: baseCfg.resolve,
output: output,
cache: true,
devtool: 'source-map',
module: {
loaders: [
baseCfg.loaders.fonts,
baseCfg.loaders.svg,
@ -28,13 +36,15 @@ config.module = {
baseCfg.loaders.js({withHot: true}),
baseCfg.loaders.scss
]
};
},
config.plugins = [
baseCfg.plugins.devBuild,
baseCfg.plugins.hotReplacement,
baseCfg.plugins.createIndexHtml,
baseCfg.plugins.vendorBundle
];
plugins: [
baseCfg.plugins.devBuild,
baseCfg.plugins.hotReplacement,
baseCfg.plugins.createIndexHtml,
baseCfg.plugins.vendorBundle
]
};
module.exports = config;
module.exports = cfg;