diff --git a/.eslintrc.js b/.eslintrc.js
index c60266d29..e26cd6534 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -28,7 +28,8 @@ module.exports = {
'new-cap': 0,
'import/no-unresolved': 'error',
'linebreak-style': 0,
- 'import/no-named-as-default': 'off'
+ 'import/no-named-as-default': 'off',
+ 'import/no-named-as-default-member': 'off'
},
settings: {
'import/resolver': {
diff --git a/Makefile b/Makefile
index dcde36241..a3191ce8a 100644
--- a/Makefile
+++ b/Makefile
@@ -37,7 +37,7 @@ generate: .prefix
$(shell npm bin)/webpack --progress --colors --bail
generate-dev: .prefix
- go-bindata -debug -pkg=server -o=server/bindata.go frontend/templates/ assets/ assets/images/
+ go-bindata -debug -pkg=server -o=server/bindata.go frontend/templates/ assets/...
$(shell npm bin)/webpack --progress --colors --bail --watch
deps:
diff --git a/assets/avatar@b3cfa572c321bac1e0bb50bfc9181d5f.svg b/assets/avatar@b3cfa572c321bac1e0bb50bfc9181d5f.svg
new file mode 100644
index 000000000..5f867b371
--- /dev/null
+++ b/assets/avatar@b3cfa572c321bac1e0bb50bfc9181d5f.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/fonts/oxygen/Oxygen-Bold.eot b/assets/fonts/oxygen/Oxygen-Bold.eot
new file mode 100644
index 000000000..13c06f158
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Bold.eot differ
diff --git a/assets/fonts/oxygen/Oxygen-Bold.ttf b/assets/fonts/oxygen/Oxygen-Bold.ttf
new file mode 100644
index 000000000..835ab0536
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Bold.ttf differ
diff --git a/assets/fonts/oxygen/Oxygen-Bold.woff b/assets/fonts/oxygen/Oxygen-Bold.woff
new file mode 100644
index 000000000..6208faa25
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Bold.woff differ
diff --git a/assets/fonts/oxygen/Oxygen-Light.eot b/assets/fonts/oxygen/Oxygen-Light.eot
new file mode 100644
index 000000000..4cd85cc65
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Light.eot differ
diff --git a/assets/fonts/oxygen/Oxygen-Light.ttf b/assets/fonts/oxygen/Oxygen-Light.ttf
new file mode 100644
index 000000000..08b9fec30
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Light.ttf differ
diff --git a/assets/fonts/oxygen/Oxygen-Light.woff b/assets/fonts/oxygen/Oxygen-Light.woff
new file mode 100644
index 000000000..bc0f9bdaf
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Light.woff differ
diff --git a/assets/fonts/oxygen/Oxygen-Regular.eot b/assets/fonts/oxygen/Oxygen-Regular.eot
new file mode 100644
index 000000000..65dceb9ca
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Regular.eot differ
diff --git a/assets/fonts/oxygen/Oxygen-Regular.ttf b/assets/fonts/oxygen/Oxygen-Regular.ttf
new file mode 100644
index 000000000..a66ddf1c8
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Regular.ttf differ
diff --git a/assets/fonts/oxygen/Oxygen-Regular.woff b/assets/fonts/oxygen/Oxygen-Regular.woff
new file mode 100644
index 000000000..c38dc5139
Binary files /dev/null and b/assets/fonts/oxygen/Oxygen-Regular.woff differ
diff --git a/assets/footer-logo@bd8b92e34e99f955afdd993acf667060.svg b/assets/footer-logo@bd8b92e34e99f955afdd993acf667060.svg
new file mode 100644
index 000000000..a3951b63d
--- /dev/null
+++ b/assets/footer-logo@bd8b92e34e99f955afdd993acf667060.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/images/.keep b/assets/images/.keep
new file mode 100644
index 000000000..e69de29bb
diff --git a/assets/images/kolide-login-logo.svg b/assets/images/kolide-logo-text.svg
similarity index 99%
rename from assets/images/kolide-login-logo.svg
rename to assets/images/kolide-logo-text.svg
index cd9504422..d0d9294b8 100644
--- a/assets/images/kolide-login-logo.svg
+++ b/assets/images/kolide-logo-text.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
diff --git a/assets/stylesheets/fonts.css b/assets/stylesheets/fonts.css
new file mode 100644
index 000000000..9141e9cbc
--- /dev/null
+++ b/assets/stylesheets/fonts.css
@@ -0,0 +1,29 @@
+@font-face {
+ font-family: 'Oxygen';
+ src: url('/assets/fonts/oxygen/Oxygen-Light.eot');
+ src: url('/assets/fonts/oxygen/Oxygen-Light.eot?#iefix') format('embedded-opentype'),
+ url('/assets/fonts/oxygen/Oxygen-Light.woff') format('woff'),
+ url('/assets/fonts/oxygen/Oxygen-Light.ttf') format('truetype');
+ font-weight: 300;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Oxygen';
+ src: url('/assets/fonts/oxygen/Oxygen-Bold.eot');
+ src: url('/assets/fonts/oxygen/Oxygen-Bold.eot?#iefix') format('embedded-opentype'),
+ url('/assets/fonts/oxygen/Oxygen-Bold.woff') format('woff'),
+ url('/assets/fonts/oxygen/Oxygen-Bold.ttf') format('truetype');
+ font-weight: bold;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Oxygen';
+ src: url('/assets/fonts/oxygen/Oxygen-Regular.eot');
+ src: url('/assets/fonts/oxygen/Oxygen-Regular.eot?#iefix') format('embedded-opentype'),
+ url('/assets/fonts/oxygen/Oxygen-Regular.woff') format('woff'),
+ url('/assets/fonts/oxygen/Oxygen-Regular.ttf') format('truetype');
+ font-weight: normal;
+ font-style: normal;
+}
diff --git a/frontend/.test.setup.js b/frontend/.test.setup.js
index bf9a6dec6..6f16c19ee 100644
--- a/frontend/.test.setup.js
+++ b/frontend/.test.setup.js
@@ -5,3 +5,27 @@ const doc = jsdom.jsdom('
');
global.document = doc;
global.window = doc.defaultView;
global.navigator = global.window.navigator;
+
+function mockStorage() {
+ const storage = {};
+
+ return {
+ setItem(key, value = '') {
+ storage[key] = value;
+ },
+ getItem(key) {
+ return storage[key];
+ },
+ removeItem(key) {
+ delete storage[key];
+ },
+ get length() {
+ return Object.keys(storage).length;
+ },
+ key(i) {
+ return Object.keys(storage)[i] || null;
+ },
+ };
+}
+
+global.localStorage = window.localStorage = mockStorage();
diff --git a/frontend/components/App/App.jsx b/frontend/components/App/App.jsx
index d43b4fc82..7d0bf64f6 100644
--- a/frontend/components/App/App.jsx
+++ b/frontend/components/App/App.jsx
@@ -1,7 +1,6 @@
import React, { Component, PropTypes } from 'react';
import { Style } from 'radium';
import Footer from './Footer';
-import componentStyles from './styles';
import globalStyles from '../../styles/global';
export class App extends Component {
@@ -11,14 +10,11 @@ export class App extends Component {
render () {
const { children } = this.props;
- const { containerStyles, childWrapperStyles } = componentStyles;
return (
-
+
-
- {children}
-
+ {children}
);
diff --git a/frontend/components/App/Footer.jsx b/frontend/components/App/Footer.jsx
index 55e73d1a0..583a8af59 100644
--- a/frontend/components/App/Footer.jsx
+++ b/frontend/components/App/Footer.jsx
@@ -1,31 +1,15 @@
import React from 'react';
import radium from 'radium';
-import Icon from '../icons/Icon';
-import styles from '../../styles';
+import componentStyles from './styles';
+import footerLogo from './footer-logo.svg';
-const { color, padding } = styles;
+const { footerStyles } = componentStyles;
const Footer = () => {
- const style = {
- container: {
- alignItems: 'center',
- backgroundColor: color.darkGrey,
- display: 'flex',
- justifyContent: 'center',
- height: '74px',
- },
- textLogo: {
- height: '20px',
- marginLeft: padding.base,
- width: '104px',
- },
- };
-
return (
-
-
-
-
+
);
};
diff --git a/frontend/components/App/Footer.tests.jsx b/frontend/components/App/Footer.tests.jsx
index 3c332fe4b..cd8d01664 100644
--- a/frontend/components/App/Footer.tests.jsx
+++ b/frontend/components/App/Footer.tests.jsx
@@ -4,17 +4,7 @@ import { mount } from 'enzyme';
import Footer from './Footer';
describe('Footer - component', () => {
- it('renders the Kolide logo', () => {
- const footer = mount(
);
- const kolideLogo = footer.find('KolideLogo');
-
- expect(kolideLogo.length).toEqual(1);
- });
-
- it('renders the Kolide text logo', () => {
- const footer = mount(
);
- const kolideTextLogo = footer.find('KolideText');
-
- expect(kolideTextLogo.length).toEqual(1);
+ it('renders', () => {
+ expect(mount(
)).toExist();
});
});
diff --git a/frontend/components/App/footer-logo.svg b/frontend/components/App/footer-logo.svg
new file mode 100644
index 000000000..a3951b63d
--- /dev/null
+++ b/frontend/components/App/footer-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/components/App/styles.js b/frontend/components/App/styles.js
index b9f56fcf6..82d07964a 100644
--- a/frontend/components/App/styles.js
+++ b/frontend/components/App/styles.js
@@ -1,9 +1,17 @@
+import styles from '../../styles';
+
+const { color } = styles;
+
export default {
- containerStyles: {
- minHeight: '100vh',
- },
- childWrapperStyles: {
- minHeight: '100vh',
- marginBottom: '-74px',
+ footerStyles: {
+ alignItems: 'center',
+ backgroundColor: color.darkGrey,
+ height: '55px',
+ textAlign: 'center',
+ position: 'absolute',
+ paddingTop: '15px',
+ left: '0',
+ right: '0',
+ bottom: '0',
},
};
diff --git a/frontend/components/forms/LoginForm/LoginForm.jsx b/frontend/components/forms/LoginForm/LoginForm.jsx
index 7a649f351..4a57ca534 100644
--- a/frontend/components/forms/LoginForm/LoginForm.jsx
+++ b/frontend/components/forms/LoginForm/LoginForm.jsx
@@ -1,7 +1,7 @@
import React, { Component, PropTypes } from 'react';
import radium from 'radium';
import componentStyles from './styles';
-import Icon from '../../icons/Icon';
+import avatar from './avatar.svg';
import InputFieldWithIcon from '../fields/InputFieldWithIcon';
class LoginForm extends Component {
@@ -14,7 +14,7 @@ class LoginForm extends Component {
this.state = {
formData: {
- email: null,
+ username: null,
password: null,
},
};
@@ -34,27 +34,38 @@ class LoginForm extends Component {
};
}
- onFormSubmit = () => {
- const { formData } = this.state;
- const { onSubmit } = this.props;
+ onFormSubmit = (evt) => {
+ evt.preventDefault();
- return onSubmit(formData);
+ if (this.canSubmit()) {
+ const { formData } = this.state;
+ const { onSubmit } = this.props;
+
+ return onSubmit(formData);
+ }
+
+ return false;
+ }
+
+ canSubmit = () => {
+ const { formData: { username, password } } = this.state;
+
+ return username && password;
}
render () {
- const { containerStyles, submitButtonStyles, userIconStyles } = componentStyles;
+ const { containerStyles, submitButtonStyles, formStyles } = componentStyles;
const { onInputChange, onFormSubmit } = this;
- const { formData } = this.state;
- const canSubmit = formData.email && formData.password;
+ const canSubmit = this.canSubmit();
return (
-
+
);
}
}
diff --git a/frontend/components/forms/LoginForm/LoginForm.tests.jsx b/frontend/components/forms/LoginForm/LoginForm.tests.jsx
index 81cd79442..03233ab6d 100644
--- a/frontend/components/forms/LoginForm/LoginForm.tests.jsx
+++ b/frontend/components/forms/LoginForm/LoginForm.tests.jsx
@@ -3,7 +3,7 @@ import expect, { createSpy, restoreSpies } from 'expect';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import LoginForm from './LoginForm';
-import { fillInFormInput } from '../../../utilities/testHelpers.js';
+import { fillInFormInput } from '../../../test/helpers';
describe('LoginForm - component', () => {
afterEach(restoreSpies);
@@ -14,16 +14,15 @@ describe('LoginForm - component', () => {
expect(form.find('InputFieldWithIcon').length).toEqual(2);
});
- it('updates component state when the email field is changed', () => {
+ it('updates component state when the username field is changed', () => {
const form = mount(
);
+ const username = 'hi@thegnar.co';
- const emailField = form.find({ name: 'email' });
- fillInFormInput(emailField, 'hello');
+ const usernameField = form.find({ name: 'username' });
+ fillInFormInput(usernameField, username);
const { formData } = form.state();
- expect(formData).toContain({
- email: 'hello',
- });
+ expect(formData).toContain({ username });
});
it('updates component state when the password field is changed', () => {
@@ -45,23 +44,25 @@ describe('LoginForm - component', () => {
submitBtn.simulate('click');
- expect(submitBtn.prop('disabled')).toEqual(true);
+ expect(submitBtn.prop('style')).toInclude({
+ cursor: 'not-allowed',
+ });
expect(submitSpy).toNotHaveBeenCalled();
});
- it('submits the form data when the submit button is clicked', () => {
+ it('submits the form data when form is submitted', () => {
const submitSpy = createSpy();
const form = mount(
);
- const emailField = form.find({ name: 'email' });
+ const usernameField = form.find({ name: 'username' });
const passwordField = form.find({ name: 'password' });
const submitBtn = form.find('button');
- fillInFormInput(emailField, 'my@email.com');
+ fillInFormInput(usernameField, 'my@email.com');
fillInFormInput(passwordField, 'p@ssw0rd');
- submitBtn.simulate('click');
+ submitBtn.simulate('submit');
expect(submitSpy).toHaveBeenCalledWith({
- email: 'my@email.com',
+ username: 'my@email.com',
password: 'p@ssw0rd',
});
});
diff --git a/frontend/components/forms/LoginForm/avatar.svg b/frontend/components/forms/LoginForm/avatar.svg
new file mode 100644
index 000000000..5f867b371
--- /dev/null
+++ b/frontend/components/forms/LoginForm/avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/components/forms/LoginForm/styles.js b/frontend/components/forms/LoginForm/styles.js
index d558d7897..d48dcd4fd 100644
--- a/frontend/components/forms/LoginForm/styles.js
+++ b/frontend/components/forms/LoginForm/styles.js
@@ -1,8 +1,7 @@
-
import styles from '../../../styles';
const { border, color, font, padding } = styles;
-const FORM_WIDTH = '480px';
+const FORM_WIDTH = '460px';
export default {
containerStyles: {
@@ -10,37 +9,42 @@ export default {
backgroundColor: color.white,
borderTopLeftRadius: border.radius.base,
borderTopRightRadius: border.radius.base,
- boxShadow: '0 0 30px 0 rgba(0,0,0,0.30)',
boxSizing: 'border-box',
display: 'flex',
flexDirection: 'column',
padding: padding.base,
width: FORM_WIDTH,
+ minHeight: '350px',
+ },
+ formStyles: {
+ boxShadow: '0 5px 30px 0 rgba(0,0,0,0.30)',
},
submitButtonStyles: (canSubmit) => {
- const bgColor = {
- start: canSubmit ? '#7166D9' : '#B2B2B2',
- end: canSubmit ? '#C86DD7' : '#C7B7C9',
- };
+ const cursor = canSubmit ? 'pointer' : 'not-allowed';
return {
- backgroundImage: `linear-gradient(to bottom right, ${bgColor.start}, ${bgColor.end})`,
+ backgroundImage: 'linear-gradient(134deg, #7166D9 0%, #C86DD7 100%)',
border: 'none',
+ cursor,
borderBottomLeftRadius: border.radius.base,
borderBottomRightRadius: border.radius.base,
boxSizing: 'border-box',
color: color.white,
- cursor: canSubmit ? 'pointer' : 'not-allowed',
fontSize: font.large,
letterSpacing: '4px',
padding: padding.base,
+ fontWeight: '300',
textTransform: 'uppercase',
width: FORM_WIDTH,
+ boxShadow: '0 3px 0 #734893',
+ position: 'relative',
+ ':active': {
+ top: '2px',
+ boxShadow: '0 1px 0 #734893, 0 -2px 0 #D1D9E9',
+ },
':focus': {
outline: 'none',
},
};
},
- userIconStyles: {
- },
};
diff --git a/frontend/components/forms/fields/InputFieldWithIcon/styles.js b/frontend/components/forms/fields/InputFieldWithIcon/styles.js
index 79918dd37..f7cddf273 100644
--- a/frontend/components/forms/fields/InputFieldWithIcon/styles.js
+++ b/frontend/components/forms/fields/InputFieldWithIcon/styles.js
@@ -20,7 +20,7 @@ export default {
borderBottomWidth: '1px',
borderBottomStyle: 'solid',
borderBottomColor: color.brightPurple,
- color: color.purple,
+ color: '#A2A1C8',
width: '378px',
':focus': {
outline: 'none',
diff --git a/frontend/components/icons/Icon.jsx b/frontend/components/icons/Icon.jsx
index 69f7968d3..913167d4a 100644
--- a/frontend/components/icons/Icon.jsx
+++ b/frontend/components/icons/Icon.jsx
@@ -1,8 +1,7 @@
import React, { Component, PropTypes } from 'react';
import radium from 'radium';
+import Check from './svg/Check';
import KolideLoginBackground from './svg/KolideLoginBackground';
-import KolideLogo from './svg/KolideLogo';
-import KolideText from './svg/KolideText';
import Lock from './svg/Lock';
import User from './svg/User';
@@ -15,9 +14,8 @@ class Icon extends Component {
};
static iconNames = {
+ check: Check,
kolideLoginBackground: KolideLoginBackground,
- kolideLogo: KolideLogo,
- kolideText: KolideText,
lock: Lock,
user: User,
};
diff --git a/frontend/components/icons/svg/Check/Check.svg.jsx b/frontend/components/icons/svg/Check/Check.svg.jsx
new file mode 100644
index 000000000..2108f39d9
--- /dev/null
+++ b/frontend/components/icons/svg/Check/Check.svg.jsx
@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import base, { basePropTypes } from '../base';
+
+class Check extends Component {
+ static propTypes = {
+ ...basePropTypes,
+ };
+
+ static variants = {
+ default: (
+
+
+
+
+
+
+
+
+
+
+
+
+ ),
+ };
+
+ render () {
+ const { alt, style, variant } = this.props;
+
+ return (
+
+ );
+ }
+}
+
+export default base(Check);
diff --git a/frontend/components/icons/svg/Check/index.js b/frontend/components/icons/svg/Check/index.js
new file mode 100644
index 000000000..e99fec10e
--- /dev/null
+++ b/frontend/components/icons/svg/Check/index.js
@@ -0,0 +1,2 @@
+export default from './Check.svg.jsx';
+
diff --git a/frontend/components/icons/svg/KolideLogo/KolideLogo.svg.jsx b/frontend/components/icons/svg/KolideLogo/KolideLogo.svg.jsx
deleted file mode 100644
index 2b51eeecb..000000000
--- a/frontend/components/icons/svg/KolideLogo/KolideLogo.svg.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, { Component } from 'react';
-import base, { basePropTypes } from '../base';
-import color from '../../../../styles/color';
-
-class KolideLogo extends Component {
- static propTypes = {
- ...basePropTypes,
- };
-
- static variants = {
- default: {
- logo: color.lightGrey,
- logoFill: color.darkGrey,
- },
- };
-
- render () {
- const { alt, style, variant } = this.props;
- const fill = KolideLogo.variants[variant];
-
- return (
-
- );
- }
-}
-
-export default base(KolideLogo);
diff --git a/frontend/components/icons/svg/KolideLogo/index.js b/frontend/components/icons/svg/KolideLogo/index.js
deleted file mode 100644
index 8e732d1ed..000000000
--- a/frontend/components/icons/svg/KolideLogo/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from './KolideLogo.svg.jsx';
diff --git a/frontend/components/icons/svg/KolideText/KolideText.svg.jsx b/frontend/components/icons/svg/KolideText/KolideText.svg.jsx
deleted file mode 100644
index fc30435dc..000000000
--- a/frontend/components/icons/svg/KolideText/KolideText.svg.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import React, { Component } from 'react';
-import base, { basePropTypes } from '../base';
-import color from '../../../../styles/color';
-
-export class KolideText extends Component {
- static propTypes = {
- ...basePropTypes,
- };
-
- static variants = {
- default: color.logoPurple,
- lightGrey: color.lightGrey,
- };
-
- render () {
- const { alt, style, variant } = this.props;
- const fill = KolideText.variants[variant];
-
- return (
-
- );
- }
-}
-
-export default base(KolideText);
diff --git a/frontend/components/icons/svg/KolideText/index.js b/frontend/components/icons/svg/KolideText/index.js
deleted file mode 100644
index 6ed67153c..000000000
--- a/frontend/components/icons/svg/KolideText/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default from './KolideText.svg.jsx';
diff --git a/frontend/components/icons/svg/User/User.svg.jsx b/frontend/components/icons/svg/User/User.svg.jsx
index 309c99a38..1baa974a9 100644
--- a/frontend/components/icons/svg/User/User.svg.jsx
+++ b/frontend/components/icons/svg/User/User.svg.jsx
@@ -21,45 +21,6 @@ export class User extends Component {
),
- circle: (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ),
colored: (
diff --git a/frontend/config/config.DEV.js b/frontend/config/config.DEV.js
new file mode 100644
index 000000000..02bc88283
--- /dev/null
+++ b/frontend/config/config.DEV.js
@@ -0,0 +1,5 @@
+import { environments } from './index.js';
+
+export default {
+ env: environments.development,
+};
diff --git a/frontend/config/config.PROD.js b/frontend/config/config.PROD.js
new file mode 100644
index 000000000..f55a76686
--- /dev/null
+++ b/frontend/config/config.PROD.js
@@ -0,0 +1,5 @@
+import { environments } from './index.js';
+
+export default {
+ env: environments.production,
+};
diff --git a/frontend/config/index.js b/frontend/config/index.js
new file mode 100644
index 000000000..dd9cf6e90
--- /dev/null
+++ b/frontend/config/index.js
@@ -0,0 +1,9 @@
+export const environments = {
+ development: 'DEV',
+ production: 'PROD',
+};
+const env = process.env.NODE_ENV || environments.development;
+const configFileLocation = `./config.${env}.js`;
+const settings = require(configFileLocation).default;
+
+export default { environments, settings };
diff --git a/frontend/kolide/endpoints.js b/frontend/kolide/endpoints.js
new file mode 100644
index 000000000..d0c21b3f8
--- /dev/null
+++ b/frontend/kolide/endpoints.js
@@ -0,0 +1,3 @@
+export default {
+ LOGIN: '/v1/kolide/login',
+};
diff --git a/frontend/kolide/index.js b/frontend/kolide/index.js
new file mode 100644
index 000000000..54f57ee6c
--- /dev/null
+++ b/frontend/kolide/index.js
@@ -0,0 +1,62 @@
+import fetch from 'isomorphic-fetch';
+import config from '../config';
+import endpoints from './endpoints';
+import local from '../utilities/local';
+
+class Kolide {
+ constructor () {
+ this.baseURL = this.setBaseURL();
+ }
+
+ setBaseURL () {
+ const {
+ settings: { env },
+ environments: { development },
+ } = config;
+
+ if (env === development) {
+ return 'http://localhost:8080/api';
+ }
+
+ throw new Error(`API base URL is not configured for environment: ${env}`);
+ }
+
+ setBearerToken (bearerToken) {
+ this.bearerToken = bearerToken;
+ }
+
+ loginUser ({ username, password }) {
+ const { LOGIN } = endpoints;
+ const endpoint = this.baseURL + LOGIN;
+
+ return fetch(endpoint, {
+ method: 'POST',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ username, password }),
+ })
+ .then(response => {
+ return response.json()
+ .then(user => {
+ if (response.ok) {
+ const { token } = user;
+
+ local.setItem('auth_token', token);
+ this.setBearerToken(token);
+
+ return user;
+ }
+
+ const error = new Error(response.statusText);
+ error.response = response;
+ error.message = user.error;
+
+ throw error;
+ });
+ });
+ }
+}
+
+export default new Kolide();
diff --git a/frontend/kolide/index.tests.js b/frontend/kolide/index.tests.js
new file mode 100644
index 000000000..f3b77356b
--- /dev/null
+++ b/frontend/kolide/index.tests.js
@@ -0,0 +1,28 @@
+import expect from 'expect';
+import Kolide from './index';
+import { validLoginRequest } from '../test/mocks';
+
+describe('Kolide - API client', () => {
+ describe('defaults', () => {
+ it('sets the base URL', () => {
+ expect(Kolide.baseURL).toEqual('http://localhost:8080/api');
+ });
+ });
+
+ describe('#loginUser', () => {
+ it('sets the bearer token', (done) => {
+ const request = validLoginRequest();
+
+ Kolide.loginUser({
+ username: 'admin',
+ password: 'secret',
+ })
+ .then(() => {
+ expect(request.isDone()).toEqual(true);
+ expect(Kolide.bearerToken).toEqual('auth_token');
+ done();
+ })
+ .catch(done);
+ });
+ });
+});
diff --git a/frontend/pages/LoginPage/LoginPage.jsx b/frontend/pages/LoginPage/LoginPage.jsx
index c37ae121c..f5c2400e9 100644
--- a/frontend/pages/LoginPage/LoginPage.jsx
+++ b/frontend/pages/LoginPage/LoginPage.jsx
@@ -1,19 +1,40 @@
-import React, { Component } from 'react';
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import { push } from 'react-router-redux';
import componentStyles from './styles';
-import Icon from '../../components/icons/Icon';
import { loadBackground, resizeBackground } from '../../utilities/backgroundImage';
+import local from '../../utilities/local';
import LoginForm from '../../components/forms/LoginForm';
+import { loginUser } from '../../redux/nodes/auth/actions';
export class LoginPage extends Component {
+ static propTypes = {
+ dispatch: PropTypes.func,
+ error: PropTypes.string,
+ loading: PropTypes.bool,
+ user: PropTypes.object,
+ };
+
componentWillMount () {
+ const { dispatch } = this.props;
const { window } = global;
+ if (local.getItem('auth_token')) {
+ return dispatch(push('/'));
+ }
+
loadBackground();
window.onresize = resizeBackground;
+
+ return false;
}
onSubmit = (formData) => {
- console.log('formData', formData);
+ const { dispatch } = this.props;
+ return dispatch(loginUser(formData))
+ .then(() => {
+ return dispatch(push('/login_successful'));
+ });
}
render () {
@@ -23,7 +44,7 @@ export class LoginPage extends Component {
return (
-
+
@@ -32,4 +53,14 @@ export class LoginPage extends Component {
}
}
-export default LoginPage;
+const mapStateToProps = (state) => {
+ const { error, loading, user } = state.auth;
+
+ return {
+ error,
+ loading,
+ user,
+ };
+};
+
+export default connect(mapStateToProps)(LoginPage);
diff --git a/frontend/pages/LoginPage/LoginPage.tests.jsx b/frontend/pages/LoginPage/LoginPage.tests.jsx
index 486e53b8d..71d3b7b7f 100644
--- a/frontend/pages/LoginPage/LoginPage.tests.jsx
+++ b/frontend/pages/LoginPage/LoginPage.tests.jsx
@@ -1,9 +1,10 @@
-import React from 'react';
import expect, { spyOn, restoreSpies } from 'expect';
import { mount } from 'enzyme';
import { noop } from 'lodash';
-import LoginPage from './LoginPage';
import * as bgImageUtility from '../../utilities/backgroundImage';
+import { connectedComponent, reduxMockStore } from '../../test/helpers';
+import local from '../../utilities/local';
+import LoginPage from './LoginPage';
describe('LoginPage - component', () => {
beforeEach(() => {
@@ -13,17 +14,35 @@ describe('LoginPage - component', () => {
afterEach(restoreSpies);
- it('renders the LoginForm', () => {
- const page = mount(
);
+ context('when the user is not logged in', () => {
+ const mockStore = reduxMockStore({ auth: {} });
- expect(page.find('LoginForm').length).toEqual(1);
+ it('renders the LoginForm', () => {
+ const page = mount(connectedComponent(LoginPage, { mockStore }));
+
+ expect(page.find('LoginForm').length).toEqual(1);
+ });
});
- it('render the Kolide Text logo', () => {
- const page = mount(
);
+ context('when the user is logged in', () => {
+ beforeEach(() => {
+ local.setItem('auth_token', 'fake-auth-token');
+ });
- expect(page.find('Icon').first().props()).toInclude({
- name: 'kolideText',
+ const user = { id: 1, firstName: 'Bill', lastName: 'Shakespeare' };
+
+ it('redirects to the home page', () => {
+ const mockStore = reduxMockStore({ auth: { user } });
+ const redirectAction = {
+ type: '@@router/CALL_HISTORY_METHOD',
+ payload: {
+ method: 'push',
+ args: ['/'],
+ },
+ };
+
+ mount(connectedComponent(LoginPage, { mockStore }));
+ expect(mockStore.getActions()).toInclude(redirectAction);
});
});
});
diff --git a/frontend/pages/LoginPage/styles.js b/frontend/pages/LoginPage/styles.js
index 300e0a904..8fc91601a 100644
--- a/frontend/pages/LoginPage/styles.js
+++ b/frontend/pages/LoginPage/styles.js
@@ -7,7 +7,7 @@ export default {
alignItems: 'center',
display: 'flex',
flexDirection: 'column',
- paddingTop: '10%',
+ paddingTop: '80px',
},
formWrapperStyles: {
alignItems: 'center',
@@ -17,8 +17,11 @@ export default {
},
whiteTabStyles: {
backgroundColor: color.white,
- height: '20px',
+ height: '30px',
marginTop: padding.base,
- width: '384px',
+ borderTopLeftRadius: '4px',
+ borderTopRightRadius: '4px',
+ boxShadow: '0 5px 30px 0 rgba(0,0,0,0.3)',
+ width: '400px',
},
};
diff --git a/frontend/pages/LoginSuccessfulPage/LoginSuccessfulPage.jsx b/frontend/pages/LoginSuccessfulPage/LoginSuccessfulPage.jsx
new file mode 100644
index 000000000..26af25c26
--- /dev/null
+++ b/frontend/pages/LoginSuccessfulPage/LoginSuccessfulPage.jsx
@@ -0,0 +1,32 @@
+import React, { Component, PropTypes } from 'react';
+import { connect } from 'react-redux';
+import componentStyles from './styles';
+import Icon from '../../components/icons/Icon';
+import { removeBackground } from '../../utilities/backgroundImage';
+
+export class LoginSuccessfulPage extends Component {
+ static propTypes = {
+ dispatch: PropTypes.func,
+ };
+
+ componentWillUnmount () {
+ removeBackground();
+ }
+
+ render () {
+ const { containerStyles, loginSuccessStyles, subtextStyles, whiteBoxStyles } = componentStyles;
+
+ return (
+
+
+
+
+
Login successful
+
Hold on to your butts.
+
+
+ );
+ }
+}
+
+export default connect()(LoginSuccessfulPage);
diff --git a/frontend/pages/LoginSuccessfulPage/index.js b/frontend/pages/LoginSuccessfulPage/index.js
new file mode 100644
index 000000000..db3c5062e
--- /dev/null
+++ b/frontend/pages/LoginSuccessfulPage/index.js
@@ -0,0 +1 @@
+export default from './LoginSuccessfulPage';
diff --git a/frontend/pages/LoginSuccessfulPage/styles.js b/frontend/pages/LoginSuccessfulPage/styles.js
new file mode 100644
index 000000000..ebb47e77e
--- /dev/null
+++ b/frontend/pages/LoginSuccessfulPage/styles.js
@@ -0,0 +1,32 @@
+import styles from '../../styles';
+
+const { color, font, padding } = styles;
+
+export default {
+ containerStyles: {
+ paddingTop: '100px',
+ textAlign: 'center',
+ },
+ loginSuccessStyles: {
+ color: color.green,
+ textTransform: 'uppercase',
+ fontSize: font.large,
+ letterSpacing: '2px',
+ fontWeight: '300',
+ },
+ subtextStyles: {
+ fontSize: font.medium,
+ color: color.lightGrey,
+ },
+ whiteBoxStyles: {
+ backgroundColor: color.white,
+ margin: '0 auto',
+ boxShadow: '0 5px 30px 0 rgba(0,0,0,0.30)',
+ borderRadius: '4px',
+ marginTop: padding.base,
+ padding: padding.base,
+ paddingTop: padding.most,
+ textAlign: 'center',
+ width: '384px',
+ },
+};
diff --git a/frontend/redux/nodes/auth/actions.js b/frontend/redux/nodes/auth/actions.js
new file mode 100644
index 000000000..999b795ad
--- /dev/null
+++ b/frontend/redux/nodes/auth/actions.js
@@ -0,0 +1,41 @@
+import Kolide from '../../../kolide';
+
+export const LOGIN_REQUEST = 'LOGIN_REQUEST';
+export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
+export const LOGIN_FAILURE = 'LOGIN_FAILURE';
+
+export const loginRequest = { type: LOGIN_REQUEST };
+export const loginSuccess = (user) => {
+ return {
+ type: LOGIN_SUCCESS,
+ payload: {
+ data: user,
+ },
+ };
+};
+export const loginFailure = (error) => {
+ return {
+ type: LOGIN_FAILURE,
+ payload: {
+ error,
+ },
+ };
+};
+
+// formData should be { username:
, password: }
+export const loginUser = (formData) => {
+ return (dispatch) => {
+ return new Promise((resolve, reject) => {
+ dispatch(loginRequest);
+ Kolide.loginUser(formData)
+ .then(user => {
+ dispatch(loginSuccess(user));
+ return resolve(user);
+ })
+ .catch(error => {
+ dispatch(loginFailure(error.message));
+ return reject(error);
+ });
+ });
+ };
+};
diff --git a/frontend/redux/nodes/auth/reducer.js b/frontend/redux/nodes/auth/reducer.js
new file mode 100644
index 000000000..1133661f0
--- /dev/null
+++ b/frontend/redux/nodes/auth/reducer.js
@@ -0,0 +1,37 @@
+import {
+ LOGIN_FAILURE,
+ LOGIN_REQUEST,
+ LOGIN_SUCCESS,
+} from './actions';
+
+export const initialState = {
+ loading: false,
+ error: null,
+ user: null,
+};
+
+const reducer = (state = initialState, action) => {
+ switch (action.type) {
+ case LOGIN_REQUEST:
+ return {
+ ...state,
+ loading: true,
+ };
+ case LOGIN_SUCCESS:
+ return {
+ ...state,
+ loading: false,
+ user: action.payload.data,
+ };
+ case LOGIN_FAILURE:
+ return {
+ ...state,
+ loading: false,
+ error: action.payload.error,
+ };
+ default:
+ return state;
+ }
+};
+
+export default reducer;
diff --git a/frontend/redux/nodes/auth/reducer.tests.js b/frontend/redux/nodes/auth/reducer.tests.js
new file mode 100644
index 000000000..6f3ba3cfc
--- /dev/null
+++ b/frontend/redux/nodes/auth/reducer.tests.js
@@ -0,0 +1,20 @@
+import expect from 'expect';
+import reducer, { initialState } from './reducer';
+import { loginRequest } from './actions';
+
+describe('Auth - reducer', () => {
+ it('sets the initial state', () => {
+ const state = reducer(undefined, { type: 'FOO' });
+
+ expect(state).toEqual(initialState);
+ });
+
+ it('changes loading to true for the userLogin action', () => {
+ const state = reducer(initialState, loginRequest);
+
+ expect(state).toEqual({
+ ...initialState,
+ loading: true,
+ });
+ });
+});
diff --git a/frontend/redux/reducers.js b/frontend/redux/reducers.js
index 05cd81ada..18f4d9e33 100644
--- a/frontend/redux/reducers.js
+++ b/frontend/redux/reducers.js
@@ -1,8 +1,10 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import app from './nodes/app/reducer';
+import auth from './nodes/auth/reducer';
export default combineReducers({
app,
+ auth,
routing: routerReducer,
});
diff --git a/frontend/router/index.jsx b/frontend/router/index.jsx
index 801216738..6d3093828 100644
--- a/frontend/router/index.jsx
+++ b/frontend/router/index.jsx
@@ -6,6 +6,7 @@ import { syncHistoryWithStore } from 'react-router-redux';
import App from '../components/App';
import HomePage from '../pages/HomePage';
import LoginPage from '../pages/LoginPage';
+import LoginSuccessfulPage from '../pages/LoginSuccessfulPage';
import store from '../redux/store';
const history = syncHistoryWithStore(browserHistory, store);
@@ -16,6 +17,7 @@ const routes = (
+
diff --git a/frontend/styles/color.js b/frontend/styles/color.js
index 9ba94f207..78973218f 100644
--- a/frontend/styles/color.js
+++ b/frontend/styles/color.js
@@ -1,12 +1,13 @@
-const grey = '#333333';
+const grey = '#66696f';
export default {
brightPurple: '#AE6DDF',
darkGrey: '#202532',
+ green: '#4FD061',
grey,
lightGrey: '#B4B4B4',
logoPurple: '#9651CA',
primary: grey,
- purple: '#B8C2E3',
- white: '#FDFDFD',
+ purple: '#c38dec',
+ white: '#FFF',
};
diff --git a/frontend/styles/font.js b/frontend/styles/font.js
index e364ba559..50f2572fc 100644
--- a/frontend/styles/font.js
+++ b/frontend/styles/font.js
@@ -2,6 +2,7 @@ import { pxToRem } from './helpers';
export default {
small: pxToRem(14),
+ medium: pxToRem(16),
base: pxToRem(18),
large: pxToRem(24),
};
diff --git a/frontend/styles/global.js b/frontend/styles/global.js
index d0843a3a3..7b51a9b4d 100644
--- a/frontend/styles/global.js
+++ b/frontend/styles/global.js
@@ -10,24 +10,31 @@ const defaultPadding = paddingLonghand(none);
export default {
...normalize,
+ html: {
+ position: 'relative',
+ minHeight: '100%',
+ },
body: {
color: color.primary,
...defaultMargin,
...defaultPadding,
- display: 'flex',
- flexDirection: 'column',
+ fontFamily: 'Oxygen, sans-serif',
fontSize: font.base,
lineHeight: 1.6,
- minHeight: '100vh',
+ margin: '0 0 94px',
},
'h1, h2, h3': {
lineHeight: 1.2,
},
'#app': {
- minHeight: '100vh',
},
'#bg': {
- position: 'absolute',
+ position: 'fixed',
+ left: 0,
+ right: 0,
+ top: 0,
+ bottom: 0,
zIndex: '-1',
+ opacity: '0.4',
},
};
diff --git a/frontend/templates/react.tmpl b/frontend/templates/react.tmpl
index d7875608c..5b8c17e50 100644
--- a/frontend/templates/react.tmpl
+++ b/frontend/templates/react.tmpl
@@ -2,6 +2,7 @@
+
Kolide
diff --git a/frontend/test/helpers.jsx b/frontend/test/helpers.jsx
new file mode 100644
index 000000000..eb18c2e19
--- /dev/null
+++ b/frontend/test/helpers.jsx
@@ -0,0 +1,30 @@
+import React from 'react';
+import configureStore from 'redux-mock-store';
+import { Provider } from 'react-redux';
+import thunk from 'redux-thunk';
+
+export const fillInFormInput = (inputComponent, value) => {
+ return inputComponent.simulate('change', { target: { value } });
+};
+
+export const reduxMockStore = (store = {}) => {
+ const middlewares = [thunk];
+ const mockStore = configureStore(middlewares);
+
+ return mockStore(store);
+};
+
+export const connectedComponent = (ComponentClass, { props = {}, mockStore }) => {
+ return (
+
+
+
+ );
+};
+
+export default {
+ connectedComponent,
+ fillInFormInput,
+ reduxMockStore,
+};
+
diff --git a/frontend/test/mocks.js b/frontend/test/mocks.js
new file mode 100644
index 000000000..74d748068
--- /dev/null
+++ b/frontend/test/mocks.js
@@ -0,0 +1,20 @@
+import nock from 'nock';
+
+export const validLoginRequest = () => {
+ return nock('http://localhost:8080')
+ .post('/api/v1/kolide/login')
+ .reply(200, {
+ token: 'auth_token',
+ id: 1,
+ username: 'admin',
+ email: 'admin@kolide.co',
+ name: '',
+ admin: true,
+ enabled: true,
+ needs_password_reset: false,
+ });
+};
+
+export default {
+ validLoginRequest,
+};
diff --git a/frontend/utilities/backgroundImage.js b/frontend/utilities/backgroundImage.js
index 970d2df88..0c71205ab 100644
--- a/frontend/utilities/backgroundImage.js
+++ b/frontend/utilities/backgroundImage.js
@@ -54,7 +54,7 @@ export const loadBackground = () => {
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const appElement = document.querySelector('#bg');
const { innerWidth } = window;
- const innerHeight = window.innerHeight - 20;
+ const innerHeight = window.innerHeight;
const unitSize = (innerWidth + innerHeight) / SHAPE_DENSITY;
svg.setAttribute('width', innerWidth);
svg.setAttribute('height', innerHeight);
@@ -142,9 +142,14 @@ export const loadBackground = () => {
refresh();
};
-export const resizeBackground = () => {
- document.querySelector('#bg svg').remove();
- clearTimeout(refreshTimeout);
- loadBackground();
+export const removeBackground = () => {
+ if (document.querySelector('#bg svg')) {
+ document.querySelector('#bg svg').remove();
+ clearTimeout(refreshTimeout);
+ }
};
+export const resizeBackground = () => {
+ removeBackground();
+ loadBackground();
+};
diff --git a/frontend/utilities/local.js b/frontend/utilities/local.js
new file mode 100644
index 000000000..624112b6d
--- /dev/null
+++ b/frontend/utilities/local.js
@@ -0,0 +1,19 @@
+import config from '../config';
+
+const { window } = global;
+const { settings } = config;
+
+export default {
+ getItem: (itemName) => {
+ const { localStorage } = window;
+ const { env } = settings;
+
+ return localStorage.getItem(`KOLIDE-${env}::${itemName}`);
+ },
+ setItem: (itemName, value) => {
+ const { localStorage } = window;
+ const { env } = settings;
+
+ return localStorage.setItem(`KOLIDE-${env}::${itemName}`, value);
+ },
+};
diff --git a/frontend/utilities/testHelpers.js b/frontend/utilities/testHelpers.js
deleted file mode 100644
index bd089d9f5..000000000
--- a/frontend/utilities/testHelpers.js
+++ /dev/null
@@ -1,7 +0,0 @@
-export const fillInFormInput = (inputComponent, value) => {
- return inputComponent.simulate('change', { target: { value } });
-};
-
-export default {
- fillInFormInput,
-};
diff --git a/package.json b/package.json
index 22c6e54db..4be8c65af 100644
--- a/package.json
+++ b/package.json
@@ -29,8 +29,10 @@
"extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.8.5",
"history": "2.0.0",
+ "isomorphic-fetch": "^2.2.1",
"jsdom": "^9.5.0",
"lodash": "^4.3.0",
+ "nock": "^8.0.0",
"postcss-functions": "^2.1.0",
"postcss-loader": "^0.8.0",
"precss": "^1.4.0",
@@ -43,7 +45,9 @@
"react-router": "^2.7.0",
"react-router-redux": "^4.0.5",
"redux": "^3.6.0",
+ "redux-mock-store": "^1.2.0",
"redux-thunk": "^2.1.0",
+ "require-hacker": "^2.1.4",
"style-loader": "^0.13.0",
"stylus-loader": "1.5.1",
"url-loader": "^0.5.7",
diff --git a/test/loaderMock.js b/test/loaderMock.js
new file mode 100644
index 000000000..1e62ec0bb
--- /dev/null
+++ b/test/loaderMock.js
@@ -0,0 +1,12 @@
+import requireHacker from 'require-hacker';
+
+const fakeComponentString = `
+ require('react').createClass({
+ render () {
+ return null;
+ }
+ })
+`;
+
+requireHacker.hook('svg', () => `module.exports = ${fakeComponentString}`);
+
diff --git a/test/mocha.opts b/test/mocha.opts
new file mode 100644
index 000000000..9b4ecc044
--- /dev/null
+++ b/test/mocha.opts
@@ -0,0 +1,3 @@
+--compilers js:babel-register
+--require test/loaderMock.js
+
diff --git a/webpack.config.js b/webpack.config.js
index da8e1afa6..c52d12921 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -29,7 +29,7 @@ var config = {
bundle: path.join(repo, 'frontend/index.jsx')
},
output: {
- path: path.join(repo, 'assets'),
+ path: path.join(repo, 'assets/'),
publicPath: "/assets/",
filename: '[name].js'
},