mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
License features (#1134)
* API client to create and get an app license * Fixes unhandled promise rejection errors in redux config * License Page and Form * Adds getLicense action * Adds License key area to App Settings Form * Use license.token instead of license.license * Implement API client * Adds key icon to License Form * Adds License Success component * Render License Success on License Page when there is a license * Adds persistent flash actions and reducer to redux * Adds nag message middleware * Moves FlashMessage component to flash_message directory * Adds Persistent Flash component * Renders Persistent Flash component from Core Layout * Adds Kyle's styles * Change license validation message * Finishing touches for app config form license area * Handle revoked licenses * License Page hits setup endpoint * Display server errors on license form * Changes 0 allowed hosts to unlimited * Trims JWT token before sending to the server * GET setup page after submitting license
This commit is contained in:
parent
ec1c3b3b94
commit
e565e03130
58 changed files with 2017 additions and 48 deletions
34
assets/images/key.svg
Normal file
34
assets/images/key.svg
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="44" height="44" viewBox="0 0 44 44">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #ed9700;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #ffce00;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #ffc200;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<title>key</title>
|
||||
<g id="Layer_2" data-name="Layer 2">
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-1" d="M30.67,1.33A13.29,13.29,0,0,0,17.82,18.18l-3.15,3.15L16,22.67h-.17L1,37.33v5.33L2.67,44H8l1.33-1.17L8,39h2.67L12,37.33V35h3V32h5l2.67-2.67L24,30.67l3.15-3.15A13.33,13.33,0,1,0,30.67,1.33ZM33.33,16a4,4,0,1,1,4-4A4,4,0,0,1,33.33,16Z"/>
|
||||
<path class="cls-2" d="M29.33,0A13.29,13.29,0,0,0,16.48,16.85L13.33,20l1.34,1.34h0L0,36v5.33L1.33,43H6.67L8,41.33,6.67,37H9.33L11,36V33h2V31h5.67l2.67-2.83,1.33,1.25,3.15-3.19A13.35,13.35,0,1,0,29.33,0ZM32,14.67a4,4,0,1,1,4-4A4,4,0,0,1,32,14.67Z"/>
|
||||
<g>
|
||||
<polygon class="cls-1" points="11 33 11 36 13.33 33 11 33"/>
|
||||
<polygon class="cls-1" points="13 31 13 33.33 16 31 13 31"/>
|
||||
<polygon class="cls-1" points="6.67 37 7.33 39 9.33 37 6.67 37"/>
|
||||
<polygon class="cls-1" points="16 24 0.01 40 0 41.33 0.67 42 17.33 25.33 16 24"/>
|
||||
<polygon class="cls-3" points="16 31 18.67 31 21.33 28.17 20 26.92 16 31"/>
|
||||
<polygon class="cls-3" points="4 43 6.67 43 8 41.5 7.33 39.58 4 43"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
26
assets/images/sign-up-pencil.svg
Normal file
26
assets/images/sign-up-pencil.svg
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="43px" height="40px" viewBox="0 0 43 40" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
|
||||
<title>sign-up-pencil</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<defs></defs>
|
||||
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Kolide-Pre-Setup-2" transform="translate(-520.000000, -633.000000)">
|
||||
<g id="Group-4" transform="translate(478.000000, 145.000000)">
|
||||
<g id="Group-2" transform="translate(18.000000, 466.000000)">
|
||||
<g id="sign-up-pencil" transform="translate(24.000000, 22.000000)">
|
||||
<path d="M25,0.0005 L25,0.0005 L1.499,0.0005 C0.671,0.0005 0,0.6705 0,1.4985 L0,38.5015 C0,39.3295 0.671,40.0005 1.499,40.0005 L30.502,40.0005 C31.329,40.0005 32,39.3295 32,38.5015 L32,7.0005 L32,7.0005 L25,7.0005 L25,0.0005 Z" id="Fill-1" fill="#4A90E2"></path>
|
||||
<polygon id="Fill-3" fill="#205492" points="25 7 32 7 25 0"></polygon>
|
||||
<path d="M20,7 L4,7 C3.448,7 3,6.552 3,6 C3,5.448 3.448,5 4,5 L20,5 C20.552,5 21,5.448 21,6 C21,6.552 20.552,7 20,7" id="Fill-5" fill="#FFFFFF"></path>
|
||||
<path d="M26,12 L4,12 C3.448,12 3,11.552 3,11 C3,10.448 3.448,10 4,10 L26,10 C26.553,10 27,10.448 27,11 C27,11.552 26.553,12 26,12" id="Fill-7" fill="#FFFFFF"></path>
|
||||
<path d="M18,17 L4,17 C3.448,17 3,16.552 3,16 C3,15.448 3.448,15 4,15 L18,15 C18.552,15 19,15.448 19,16 C19,16.552 18.552,17 18,17" id="Fill-9" fill="#FFFFFF"></path>
|
||||
<path d="M10,35 L4,35 C3.448,35 3,34.553 3,34 C3,33.447 3.448,33 4,33 L10,33 C10.552,33 11,33.447 11,34 C11,34.553 10.552,35 10,35" id="Fill-11" fill="#FFFFFF"></path>
|
||||
<polygon id="Fill-13" fill="#FFFFFF" points="15 29 21 35 15 35"></polygon>
|
||||
<polygon id="Fill-15" fill="#FFAD00" points="16 28 22 34 38 18 32 12"></polygon>
|
||||
<path d="M41.584,11.5845 L38.416,8.4155 C37.634,7.6335 36.366,7.6335 35.584,8.4155 L33,11.0005 L39,17.0005 L41.584,14.4155 C42.366,13.6335 42.366,12.3665 41.584,11.5845" id="Fill-17" fill="#FF5850"></path>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.4 KiB |
|
|
@ -41,9 +41,9 @@ export default (WrappedComponent) => {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { isLoadingUser } = this.props;
|
||||
const { currentUser, isLoadingUser } = this.props;
|
||||
|
||||
if (isLoadingUser) {
|
||||
if (isLoadingUser || currentUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
43
frontend/components/LicenseSuccess/LicenseSuccess.jsx
Normal file
43
frontend/components/LicenseSuccess/LicenseSuccess.jsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import Icon from 'components/icons/Icon';
|
||||
import licenseInterface from 'interfaces/license';
|
||||
|
||||
import key from '../../../assets/images/key.svg';
|
||||
|
||||
const baseClass = 'license-success';
|
||||
|
||||
const LicenseSuccess = ({ license }) => {
|
||||
const { allowed_hosts: allowedHosts, expiry } = license;
|
||||
const expiryMoment = moment(expiry);
|
||||
const timeToExpiration = expiryMoment.toNow(true);
|
||||
const hostText = allowedHosts.count === 1 ? 'Host' : 'Hosts';
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<div className={`${baseClass}__container`}>
|
||||
<h2><Icon name="success-check" />License Upload Successful!</h2>
|
||||
<h3>
|
||||
<img alt="Kolide License" className={`${baseClass}__key-img`} src={key} />
|
||||
Kolide License Details:
|
||||
</h3>
|
||||
<h4>Current License Level:</h4>
|
||||
<ul>
|
||||
<li><Icon name="single-host" />{allowedHosts} {hostText}</li>
|
||||
{timeToExpiration && <li><Icon name="clock" />Expires in {timeToExpiration}</li>}
|
||||
</ul>
|
||||
<a href="/setup" className="button button--success">
|
||||
SETUP KOLIDE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LicenseSuccess.propTypes = {
|
||||
license: licenseInterface.isRequired,
|
||||
};
|
||||
|
||||
export default LicenseSuccess;
|
||||
|
||||
18
frontend/components/LicenseSuccess/LicenseSuccess.tests.jsx
Normal file
18
frontend/components/LicenseSuccess/LicenseSuccess.tests.jsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
import expect from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { licenseStub } from 'test/stubs';
|
||||
import LicenseSuccess from 'components/LicenseSuccess';
|
||||
|
||||
const defaultProps = {
|
||||
license: licenseStub(),
|
||||
};
|
||||
|
||||
describe('LicenseSuccess - component', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders', () => {
|
||||
expect(mount(<LicenseSuccess {...defaultProps} />).length).toEqual(1, 'Expected LicenseSuccess component to render');
|
||||
});
|
||||
});
|
||||
});
|
||||
87
frontend/components/LicenseSuccess/_styles.scss
Normal file
87
frontend/components/LicenseSuccess/_styles.scss
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
.license-success {
|
||||
@include display(flex);
|
||||
@include align-content(center);
|
||||
@include justify-content(center);
|
||||
@include flex-grow(1);
|
||||
padding: 20px;
|
||||
|
||||
h2 {
|
||||
@include display(flex);
|
||||
border-bottom: 1px solid $accent-medium;
|
||||
color: #48c586;
|
||||
font-size: 24px;
|
||||
font-weight: $bold;
|
||||
line-height: 50px;
|
||||
margin-top: 0;
|
||||
padding-bottom: 15px;
|
||||
|
||||
i {
|
||||
font-size: 50px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include display(flex);
|
||||
color: $link;
|
||||
font-size: 24px;
|
||||
font-weight: $bold;
|
||||
line-height: 43px;
|
||||
margin-top: 0;
|
||||
|
||||
img {
|
||||
height: 43px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: #48c586;
|
||||
font-size: 16px;
|
||||
font-weight: $bold;
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include align-self(center);
|
||||
@include size(500px auto);
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
padding: 25px 35px;
|
||||
margin-top: -55px;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 0;
|
||||
text-decoration: none;
|
||||
|
||||
li {
|
||||
@include display(flex);
|
||||
font-size: 24px;
|
||||
line-height: 48px;
|
||||
margin-bottom: 30px;
|
||||
|
||||
i {
|
||||
font-size: 48px;
|
||||
margin-right: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
background-color: #48c586;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 6px 0 rgba(32, 36, 50, 0.24), 0 6px 0 0 #1d9056;
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: $bold;
|
||||
height: 60px;
|
||||
line-height: 60px;
|
||||
|
||||
&:hover {
|
||||
background-color: #48c586;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/components/LicenseSuccess/index.js
Normal file
1
frontend/components/LicenseSuccess/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './LicenseSuccess';
|
||||
|
|
@ -65,6 +65,12 @@ $base-class: 'button';
|
|||
@include button-variant($alert);
|
||||
}
|
||||
|
||||
&--muted {
|
||||
@include button-variant(#eceef1, null, $footer-accent);
|
||||
|
||||
color: $footer-accent;
|
||||
}
|
||||
|
||||
&--warning {
|
||||
@include button-variant($warning);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
import classnames from 'classnames';
|
||||
|
||||
import Icon from 'components/icons/Icon';
|
||||
|
||||
const baseClass = 'persistent-flash';
|
||||
|
||||
const PersistentFlash = ({ message }) => {
|
||||
const klass = classnames(baseClass, `${baseClass}--error`);
|
||||
|
||||
return (
|
||||
<div className={klass}>
|
||||
<div className={`${baseClass}__content`}>
|
||||
<Icon name="warning-filled" /> <span>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PersistentFlash.propTypes = {
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PersistentFlash;
|
||||
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
.wrapper {
|
||||
> div {
|
||||
&.persistent-flash {
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
max-height: 50px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.persistent-flash {
|
||||
@include display(flex);
|
||||
@include align-items(center);
|
||||
@include justify-content(center);
|
||||
@include position(fixed, 0 0 null $nav-width);
|
||||
color: $white;
|
||||
padding: $pad-half;
|
||||
z-index: 2;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.25);
|
||||
background-color: #3d4758;
|
||||
min-width: $min-width;
|
||||
|
||||
@include breakpoint(smalldesk) {
|
||||
left: $nav-tablet-width;
|
||||
}
|
||||
|
||||
&__content {
|
||||
@include flex-grow(1);
|
||||
text-align: center;
|
||||
|
||||
i {
|
||||
color: $alert;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 15px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
export default from './PersistentFlash';
|
||||
|
|
@ -17,10 +17,101 @@
|
|||
}
|
||||
}
|
||||
|
||||
&__license-btn {
|
||||
width: 200px;
|
||||
|
||||
&--reset {
|
||||
margin-right: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
&__license-detail {
|
||||
&-icon {
|
||||
i {
|
||||
color: rgba(32, 36, 50, 0.54);
|
||||
font-size: 48px;
|
||||
margin-right: 23px;
|
||||
}
|
||||
}
|
||||
|
||||
&-text-wrapper {
|
||||
i {
|
||||
color: $alert;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&-text {
|
||||
color: rgba(32, 37, 50, 0.5);
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&-warning {
|
||||
color: $alert;
|
||||
font-size: 15px;
|
||||
font-weight: $bold;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&__license-form {
|
||||
float: right;
|
||||
|
||||
a {
|
||||
float: right;
|
||||
font-size: 15px;
|
||||
line-height: 25px;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
@include display(inline-flex);
|
||||
color: #858495;
|
||||
font-size: 20px;
|
||||
font-weight: $normal;
|
||||
line-height: 25px;
|
||||
margin-top: 0;
|
||||
|
||||
img {
|
||||
height: 25px;
|
||||
margin-right: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-family: SourceCodePro, $monospace;
|
||||
resize: none;
|
||||
}
|
||||
}
|
||||
|
||||
&__license-info {
|
||||
float: left;
|
||||
|
||||
&-row {
|
||||
@include display(flex);
|
||||
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
}
|
||||
|
||||
&__section {
|
||||
@include clearfix;
|
||||
margin: 0 0 30px;
|
||||
|
||||
.app-config-form__license-input {
|
||||
@include size(424px 160px);
|
||||
|
||||
background-color: $bg-medium;
|
||||
border: 1px solid rgba(73, 143, 226, 0.24);
|
||||
border-radius: 4px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.smtp-options {
|
||||
font-size: 15px;
|
||||
font-weight: $bold;
|
||||
|
|
|
|||
64
frontend/components/forms/LicenseForm/LicenseForm.jsx
Normal file
64
frontend/components/forms/LicenseForm/LicenseForm.jsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Form from 'components/forms/Form';
|
||||
import formFieldInterface from 'interfaces/form_field';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
import validate from 'components/forms/LicenseForm/validate';
|
||||
|
||||
import freeTrial from '../../../../assets/images/sign-up-pencil.svg';
|
||||
import key from '../../../../assets/images/key.svg';
|
||||
|
||||
const fields = ['license'];
|
||||
const baseClass = 'license-form';
|
||||
|
||||
class LicenseForm extends Component {
|
||||
static propTypes = {
|
||||
baseError: PropTypes.string,
|
||||
fields: PropTypes.shape({
|
||||
license: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
render () {
|
||||
const { baseError, fields: formFields, handleSubmit } = this.props;
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
<div className={`${baseClass}__container`}>
|
||||
<h2>
|
||||
<img alt="Kolide License" className={`${baseClass}__key-img`} src={key} />
|
||||
Kolide License
|
||||
</h2>
|
||||
{baseError && <div className="form__base-error">{baseError}</div>}
|
||||
<InputField
|
||||
{...formFields.license}
|
||||
hint={<p className={`${baseClass}__help-text`}>Found under <a href="https://www.kolide.co/account">Account Settings</a> at Kolide.co</p>}
|
||||
inputClassName={`${baseClass}__input`}
|
||||
label="Enter License File"
|
||||
type="textarea"
|
||||
/>
|
||||
<Button block className={`${baseClass}__upload-btn`} type="submit">
|
||||
UPLOAD LICENSE
|
||||
</Button>
|
||||
<p className="form-field__label">Don't have a license?</p>
|
||||
<p className={`${baseClass}__free-trial-text`}>Start a free trial of Kolide today!</p>
|
||||
<a
|
||||
className={`${baseClass}__free-trial-btn button button--unstyled`}
|
||||
href="https://www.kolide.co/register"
|
||||
>
|
||||
<img
|
||||
alt="Free trial"
|
||||
src={freeTrial}
|
||||
className={`${baseClass}__free-trial-img`}
|
||||
/>
|
||||
<span>Sign up for Free Kolide Trial</span>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form(LicenseForm, { fields, validate });
|
||||
59
frontend/components/forms/LicenseForm/LicenseForm.tests.jsx
Normal file
59
frontend/components/forms/LicenseForm/LicenseForm.tests.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import React from 'react';
|
||||
import expect, { createSpy, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
import { fillInFormInput, itBehavesLikeAFormInputElement } from 'test/helpers';
|
||||
import LicenseForm from 'components/forms/LicenseForm';
|
||||
|
||||
const defaultProps = {
|
||||
handleSubmit: noop,
|
||||
};
|
||||
|
||||
describe('LicenseForm - component', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders', () => {
|
||||
expect(mount(<LicenseForm {...defaultProps} />).length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('license input', () => {
|
||||
const Form = mount(<LicenseForm {...defaultProps} />);
|
||||
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(Form, 'license', 'textarea');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting the form', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('calls the handleSubmit prop when valid', () => {
|
||||
const spy = createSpy();
|
||||
const props = { handleSubmit: spy };
|
||||
const Form = mount(<LicenseForm {...props} />);
|
||||
const LicenseField = Form.find({ name: 'license' }).find('textarea');
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
|
||||
fillInFormInput(LicenseField, jwtToken);
|
||||
|
||||
Form.simulate('submit');
|
||||
|
||||
expect(spy).toHaveBeenCalledWith({ license: jwtToken });
|
||||
});
|
||||
|
||||
it('does not submit when invalid', () => {
|
||||
const spy = createSpy();
|
||||
const props = { handleSubmit: spy };
|
||||
const Form = mount(<LicenseForm {...props} />);
|
||||
const LicenseField = Form.find({ name: 'license' }).find('textarea');
|
||||
const jwtToken = 'invalid.token';
|
||||
|
||||
fillInFormInput(LicenseField, jwtToken);
|
||||
|
||||
Form.simulate('submit');
|
||||
|
||||
expect(spy).toNotHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
99
frontend/components/forms/LicenseForm/_styles.scss
Normal file
99
frontend/components/forms/LicenseForm/_styles.scss
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
.license-form {
|
||||
@include display(flex);
|
||||
@include align-content(center);
|
||||
@include justify-content(center);
|
||||
@include flex-grow(1);
|
||||
|
||||
textarea {
|
||||
resize: none;
|
||||
}
|
||||
|
||||
&__key-img {
|
||||
height: 43px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include display(flex);
|
||||
color: $link;
|
||||
font-size: 24px;
|
||||
font-weight: $bold;
|
||||
line-height: 43px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&__container {
|
||||
@include align-self(center);
|
||||
@include size(500px auto);
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: 0 0 30px 0 rgba(0, 0, 0, 0.3);
|
||||
box-sizing: border-box;
|
||||
padding: 25px 35px;
|
||||
margin-top: -55px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__free-trial-btn {
|
||||
@include display(flex);
|
||||
@include align-items(center);
|
||||
|
||||
background-color: $white;
|
||||
border: 1px solid $accent-light;
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 3px 12px 0 rgba(49, 49, 93, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.07);
|
||||
height: 90px;
|
||||
padding-left: 24px;
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
|
||||
&:hover {
|
||||
background-color: $bg-light;
|
||||
box-shadow: 0 3px 12px 0 rgba(49, 49, 93, 0.1), 0 3px 3px 0 rgba(0, 0, 0, 0.07);
|
||||
}
|
||||
|
||||
span {
|
||||
color: $link;
|
||||
font-size: 18px;
|
||||
margin-left: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
&__free-trial-text {
|
||||
font-size: 15px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&__help-text {
|
||||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
|
||||
span {
|
||||
color: $link;
|
||||
}
|
||||
}
|
||||
|
||||
&__input {
|
||||
background-color: $bg-light;
|
||||
border-radius: 4px;
|
||||
box-shadow: inset 0 0 5px 0 rgba(32, 36, 50, 0.24);
|
||||
font-family: SourceCodePro, $monospace;
|
||||
line-height: 28px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__upload-btn {
|
||||
@include button-variant(#48c586);
|
||||
|
||||
font-size: 18px;
|
||||
height: 60px;
|
||||
}
|
||||
}
|
||||
1
frontend/components/forms/LicenseForm/index.js
Normal file
1
frontend/components/forms/LicenseForm/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './LicenseForm';
|
||||
19
frontend/components/forms/LicenseForm/validate.js
Normal file
19
frontend/components/forms/LicenseForm/validate.js
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { size, trim } from 'lodash';
|
||||
|
||||
import validJwtToken from 'components/forms/validators/valid_jwt_token';
|
||||
|
||||
export default ({ license }) => {
|
||||
const errors = {};
|
||||
|
||||
if (!license) {
|
||||
errors.license = 'License must be present';
|
||||
}
|
||||
|
||||
if (license && !validJwtToken(trim(license))) {
|
||||
errors.license = 'License syntax is not valid. Please ensure you have entered the entire license. Please contact support@kolide.co if you need assistance';
|
||||
}
|
||||
|
||||
const valid = !size(errors);
|
||||
|
||||
return { errors, valid };
|
||||
};
|
||||
31
frontend/components/forms/LicenseForm/validate.tests.js
Normal file
31
frontend/components/forms/LicenseForm/validate.tests.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import validate from 'components/forms/LicenseForm/validate';
|
||||
|
||||
describe('LicenseForm - validation', () => {
|
||||
it('is valid given a valid license', () => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
const { errors, valid } = validate({ license: jwtToken });
|
||||
|
||||
expect(valid).toEqual(true);
|
||||
expect(errors).toEqual({});
|
||||
});
|
||||
|
||||
it('is not valid if the license is blank', () => {
|
||||
const jwtToken = '';
|
||||
const { errors, valid } = validate({ license: jwtToken });
|
||||
|
||||
expect(valid).toEqual(false);
|
||||
expect(errors).toEqual({ license: 'License must be present' });
|
||||
});
|
||||
|
||||
it('is not valid if the license is invalid', () => {
|
||||
const jwtToken = 'KFBR392';
|
||||
const { errors, valid } = validate({ license: jwtToken });
|
||||
|
||||
expect(valid).toEqual(false);
|
||||
expect(errors).toEqual({
|
||||
license: 'License syntax is not valid. Please ensure you have entered the entire license. Please contact support@kolide.co if you need assistance',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import moment from 'moment';
|
||||
|
||||
import Button from 'components/buttons/Button';
|
||||
import Checkbox from 'components/forms/fields/Checkbox';
|
||||
|
|
@ -7,10 +8,13 @@ import Form from 'components/forms/Form';
|
|||
import formFieldInterface from 'interfaces/form_field';
|
||||
import Icon from 'components/icons/Icon';
|
||||
import InputField from 'components/forms/fields/InputField';
|
||||
import licenseInterface from 'interfaces/license';
|
||||
import OrgLogoIcon from 'components/icons/OrgLogoIcon';
|
||||
import Slider from 'components/forms/fields/Slider';
|
||||
import validate from 'components/forms/admin/AppConfigForm/validate';
|
||||
|
||||
import key from '../../../../../assets/images/key.svg';
|
||||
|
||||
const authMethodOptions = [
|
||||
{ label: 'Plain', value: 'authmethod_plain' },
|
||||
{ label: 'Cram MD5', value: 'authmethod_cram_md5' },
|
||||
|
|
@ -22,8 +26,8 @@ const authTypeOptions = [
|
|||
const baseClass = 'app-config-form';
|
||||
const formFields = [
|
||||
'authentication_method', 'authentication_type', 'domain', 'enable_ssl_tls', 'enable_start_tls',
|
||||
'kolide_server_url', 'org_logo_url', 'org_name', 'osquery_enroll_secret', 'password', 'port', 'sender_address',
|
||||
'server', 'user_name', 'verify_ssl_certs',
|
||||
'kolide_server_url', 'license', 'org_logo_url', 'org_name', 'osquery_enroll_secret', 'password',
|
||||
'port', 'sender_address', 'server', 'user_name', 'verify_ssl_certs',
|
||||
];
|
||||
const Header = ({ showAdvancedOptions }) => {
|
||||
const CaratIcon = <Icon name={showAdvancedOptions ? 'downcarat' : 'upcarat'} />;
|
||||
|
|
@ -42,6 +46,7 @@ class AppConfigForm extends Component {
|
|||
enable_ssl_tls: formFieldInterface.isRequired,
|
||||
enable_start_tls: formFieldInterface.isRequired,
|
||||
kolide_server_url: formFieldInterface.isRequired,
|
||||
license: formFieldInterface.isRequired,
|
||||
org_logo_url: formFieldInterface.isRequired,
|
||||
org_name: formFieldInterface.isRequired,
|
||||
password: formFieldInterface.isRequired,
|
||||
|
|
@ -51,7 +56,9 @@ class AppConfigForm extends Component {
|
|||
user_name: formFieldInterface.isRequired,
|
||||
verify_ssl_certs: formFieldInterface.isRequired,
|
||||
}).isRequired,
|
||||
handleSubmit: PropTypes.func,
|
||||
handleSubmit: PropTypes.func.isRequired,
|
||||
handleUpdateLicense: PropTypes.func.isRequired,
|
||||
license: licenseInterface.isRequired,
|
||||
smtpConfigured: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
|
|
@ -61,6 +68,14 @@ class AppConfigForm extends Component {
|
|||
this.state = { revealSecret: false, showAdvancedOptions: false };
|
||||
}
|
||||
|
||||
onResetLicense = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { fields, license } = this.props;
|
||||
|
||||
return fields.license.onChange(license.token);
|
||||
}
|
||||
|
||||
onToggleAdvancedOptions = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
|
|
@ -81,6 +96,14 @@ class AppConfigForm extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onUpdateLicense = (evt) => {
|
||||
evt.preventDefault();
|
||||
|
||||
const { fields, handleUpdateLicense } = this.props;
|
||||
|
||||
return handleUpdateLicense(fields.license.value);
|
||||
}
|
||||
|
||||
renderAdvancedOptions = () => {
|
||||
const { fields } = this.props;
|
||||
const { showAdvancedOptions } = this.state;
|
||||
|
|
@ -137,9 +160,13 @@ class AppConfigForm extends Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { fields, handleSubmit, smtpConfigured } = this.props;
|
||||
const { onToggleAdvancedOptions, onToggleRevealSecret, renderAdvancedOptions, renderSmtpSection } = this;
|
||||
const { fields, handleSubmit, license, smtpConfigured } = this.props;
|
||||
const { onToggleAdvancedOptions, onResetLicense, onToggleRevealSecret, onUpdateLicense, renderAdvancedOptions, renderSmtpSection } = this;
|
||||
const { revealSecret, showAdvancedOptions } = this.state;
|
||||
const expiryMoment = license && moment(license.expiry);
|
||||
const hostWarning = license.hosts > license.allowed_hosts;
|
||||
const expiryWarning = expiryMoment && expiryMoment.diff(moment(), 'days') <= 2;
|
||||
const timeToExpiration = expiryMoment.toNow(true);
|
||||
|
||||
return (
|
||||
<form className={baseClass} onSubmit={handleSubmit}>
|
||||
|
|
@ -160,6 +187,57 @@ class AppConfigForm extends Component {
|
|||
<p>Avatar Preview</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Kolide License</h2>
|
||||
<div className={`${baseClass}__license-info`}>
|
||||
<div className={`${baseClass}__license-info-row`}>
|
||||
<div className={`${baseClass}__license-detail-icon`}><Icon name="business" /></div>
|
||||
<div className={`${baseClass}__license-detail-text-wrapper`}>
|
||||
<p className={`${baseClass}__license-detail-text`}>{license.organization}</p>
|
||||
{hostWarning && <p className={`${baseClass}__license-detail-warning`}>Exceeding Host Limit</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__license-info-row`}>
|
||||
<div className={`${baseClass}__license-detail-icon`}><Icon name="single-host" /></div>
|
||||
<div className={`${baseClass}__license-detail-text-wrapper`}>
|
||||
<p className={`${baseClass}__license-detail-text`}>{license.hosts}/{license.allowed_hosts} Hosts {hostWarning && <Icon name="warning-filled" />}</p>
|
||||
{hostWarning && <p className={`${baseClass}__license-detail-warning`}>Exceeding Host Limit</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__license-info-row`}>
|
||||
<div className={`${baseClass}__license-detail-icon`}><Icon name="clock" /></div>
|
||||
<div className={`${baseClass}__license-detail-text-wrapper`}>
|
||||
<p className={`${baseClass}__license-detail-text`}>{timeToExpiration} {expiryWarning && <Icon name="warning-filled" />}</p>
|
||||
{expiryWarning && <p className={`${baseClass}__license-detail-warning`}>Subscription Expiring Soon!</p>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__license-form`}>
|
||||
<h3><img alt="License String" src={key} />License String</h3>
|
||||
<a href="https://www.kolide.co/account" rel="noopener noreferrer" target="_blank">
|
||||
View License Options <Icon name="external-link" />
|
||||
</a>
|
||||
<InputField
|
||||
{...fields.license}
|
||||
inputClassName={`${baseClass}__license-input`}
|
||||
type="textarea"
|
||||
/>
|
||||
<Button
|
||||
className={`${baseClass}__license-btn ${baseClass}__license-btn--reset`}
|
||||
onClick={onResetLicense}
|
||||
variant="muted"
|
||||
>
|
||||
CANCEL
|
||||
</Button>
|
||||
<Button
|
||||
className={`${baseClass}__license-btn ${baseClass}__license-btn--save`}
|
||||
onClick={onUpdateLicense}
|
||||
variant="success"
|
||||
>
|
||||
SAVE CHANGES
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`${baseClass}__section`}>
|
||||
<h2>Kolide Web Address</h2>
|
||||
<div className={`${baseClass}__inputs`}>
|
||||
|
|
|
|||
|
|
@ -5,9 +5,23 @@ import { noop } from 'lodash';
|
|||
|
||||
import AppConfigForm from 'components/forms/admin/AppConfigForm';
|
||||
import { itBehavesLikeAFormInputElement } from 'test/helpers';
|
||||
import { licenseStub } from 'test/stubs';
|
||||
|
||||
describe('AppConfigForm - form', () => {
|
||||
const form = mount(<AppConfigForm handleSubmit={noop} smtpConfigured={false} />);
|
||||
const defaultProps = {
|
||||
formData: { org_name: 'Kolide' },
|
||||
handleSubmit: noop,
|
||||
handleUpdateLicense: noop,
|
||||
license: licenseStub(),
|
||||
smtpConfigured: false,
|
||||
};
|
||||
const form = mount(<AppConfigForm {...defaultProps} />);
|
||||
|
||||
describe('License input', () => {
|
||||
it('renders an input field', () => {
|
||||
itBehavesLikeAFormInputElement(form, 'license', 'textarea');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Organization Name input', () => {
|
||||
it('renders an input field', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { size, some } from 'lodash';
|
||||
import { size, some, trim } from 'lodash';
|
||||
|
||||
import APP_CONSTANTS from 'app_constants';
|
||||
import validJwtToken from 'components/forms/validators/valid_jwt_token';
|
||||
|
||||
const { APP_SETTINGS } = APP_CONSTANTS;
|
||||
|
||||
|
|
@ -9,6 +10,7 @@ export default (formData) => {
|
|||
const {
|
||||
authentication_type: authType,
|
||||
kolide_server_url: kolideServerUrl,
|
||||
license,
|
||||
org_name: orgName,
|
||||
password: smtpPassword,
|
||||
sender_address: smtpSenderAddress,
|
||||
|
|
@ -21,6 +23,14 @@ export default (formData) => {
|
|||
errors.kolide_server_url = 'Kolide Server URL must be present';
|
||||
}
|
||||
|
||||
if (!license) {
|
||||
errors.license = 'License must be present';
|
||||
}
|
||||
|
||||
if (license && !validJwtToken(trim(license))) {
|
||||
errors.license = 'License is not a valid JWT token';
|
||||
}
|
||||
|
||||
if (!orgName) {
|
||||
errors.org_name = 'Organization Name must be present';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ describe('AppConfigForm - validations', () => {
|
|||
org_name: 'The Gnar Co.',
|
||||
authentication_type: 'username_password',
|
||||
kolide_server_url: 'https://gnar.dog',
|
||||
license: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
|
||||
sender_address: 'hi@gnar.dog',
|
||||
server: '192.168.99.100',
|
||||
port: 1025,
|
||||
|
|
@ -18,6 +19,34 @@ describe('AppConfigForm - validations', () => {
|
|||
expect(validate(validFormData)).toEqual({ valid: true, errors: {} });
|
||||
});
|
||||
|
||||
it('validates presense of the license field', () => {
|
||||
const invalidFormData = {
|
||||
...validFormData,
|
||||
license: '',
|
||||
};
|
||||
|
||||
expect(validate(invalidFormData)).toEqual({
|
||||
valid: false,
|
||||
errors: {
|
||||
license: 'License must be present',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('validates the license is a JWT token', () => {
|
||||
const invalidFormData = {
|
||||
...validFormData,
|
||||
license: 'KFBR392',
|
||||
};
|
||||
|
||||
expect(validate(invalidFormData)).toEqual({
|
||||
valid: false,
|
||||
errors: {
|
||||
license: 'License is not a valid JWT token',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('validates presence of the org_name field', () => {
|
||||
const invalidFormData = {
|
||||
...validFormData,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
const JWT_REGEX = /^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/;
|
||||
|
||||
export default (jwtToken) => {
|
||||
return JWT_REGEX.test(jwtToken);
|
||||
};
|
||||
10
frontend/interfaces/license.js
Normal file
10
frontend/interfaces/license.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { PropTypes } from 'react';
|
||||
|
||||
export default PropTypes.shape({
|
||||
allowed_hosts: PropTypes.oneOfType([
|
||||
PropTypes.string, PropTypes.number,
|
||||
]),
|
||||
expiry: PropTypes.string,
|
||||
hosts: PropTypes.number,
|
||||
license: PropTypes.string,
|
||||
});
|
||||
|
|
@ -12,6 +12,7 @@ export default {
|
|||
LABEL_HOSTS: (id) => {
|
||||
return `/v1/kolide/labels/${id}/hosts`;
|
||||
},
|
||||
LICENSE: '/v1/kolide/license',
|
||||
LOGIN: '/v1/kolide/login',
|
||||
LOGOUT: '/v1/kolide/logout',
|
||||
ME: '/v1/kolide/me',
|
||||
|
|
@ -24,6 +25,7 @@ export default {
|
|||
return `/v1/kolide/packs/${pack.id}/scheduled`;
|
||||
},
|
||||
SETUP: '/v1/setup',
|
||||
SETUP_LICENSE: '/v1/license',
|
||||
STATUS_LABEL_COUNTS: '/v1/kolide/host_summary',
|
||||
TARGETS: '/v1/kolide/targets',
|
||||
USERS: '/v1/kolide/users',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,12 @@ export const formatSelectedTargetsForApi = (selectedTargets, appendID = false) =
|
|||
return { hosts, labels };
|
||||
};
|
||||
|
||||
const parseLicense = (license) => {
|
||||
const allowedHosts = license.allowed_hosts === 0 ? 'Unlimited' : license.allowed_hosts;
|
||||
|
||||
return { ...license, allowed_hosts: allowedHosts };
|
||||
};
|
||||
|
||||
const setupData = (formData) => {
|
||||
const orgInfo = pick(formData, ORG_INFO_ATTRS);
|
||||
const adminInfo = pick(formData, ADMIN_ATTRS);
|
||||
|
|
@ -79,4 +85,11 @@ const setupData = (formData) => {
|
|||
};
|
||||
};
|
||||
|
||||
export default { addGravatarUrlToResource, formatConfigDataForServer, formatSelectedTargetsForApi, labelSlug, setupData };
|
||||
export default {
|
||||
addGravatarUrlToResource,
|
||||
formatConfigDataForServer,
|
||||
formatSelectedTargetsForApi,
|
||||
labelSlug,
|
||||
parseLicense,
|
||||
setupData,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import expect from 'expect';
|
||||
import { omit } from 'lodash';
|
||||
|
||||
import { configStub } from 'test/stubs';
|
||||
import { configStub, licenseStub } from 'test/stubs';
|
||||
import helpers from 'kolide/helpers';
|
||||
|
||||
const label1 = { id: 1, target_type: 'labels' };
|
||||
|
|
@ -10,6 +10,25 @@ const host1 = { id: 6, target_type: 'hosts' };
|
|||
const host2 = { id: 5, target_type: 'hosts' };
|
||||
|
||||
describe('Kolide API - helpers', () => {
|
||||
describe('#parseLicense', () => {
|
||||
const { parseLicense } = helpers;
|
||||
const validLicense = licenseStub();
|
||||
|
||||
it('returns Unlimited when allowed_hosts is 0', () => {
|
||||
const unlimitedLicense = { ...validLicense, allowed_hosts: 0 };
|
||||
|
||||
expect(parseLicense(unlimitedLicense)).toEqual({
|
||||
...validLicense,
|
||||
allowed_hosts: 'Unlimited',
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the allowed_hosts attribute when not 0', () => {
|
||||
const limitedLicense = { ...validLicense, allowed_hosts: 2 };
|
||||
|
||||
expect(parseLicense(limitedLicense)).toEqual(limitedLicense);
|
||||
});
|
||||
});
|
||||
describe('#labelSlug', () => {
|
||||
it('creates a slug for the label', () => {
|
||||
expect(helpers.labelSlug({ display_text: 'All Hosts' })).toEqual('all-hosts');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { get, omit } from 'lodash';
|
||||
import { get, omit, trim } from 'lodash';
|
||||
|
||||
import { appendTargetTypeToTargets } from 'redux/nodes/entities/targets/helpers';
|
||||
import Base from 'kolide/base';
|
||||
|
|
@ -120,6 +120,28 @@ class Kolide extends Base {
|
|||
},
|
||||
}
|
||||
|
||||
license = {
|
||||
setup: (jwtToken) => {
|
||||
const { SETUP_LICENSE } = endpoints;
|
||||
|
||||
return this.authenticatedPost(this.endpoint(SETUP_LICENSE), JSON.stringify({ license: trim(jwtToken) }))
|
||||
.then(response => helpers.parseLicense(response.license));
|
||||
},
|
||||
create: (jwtToken) => {
|
||||
const { LICENSE } = endpoints;
|
||||
|
||||
return this.authenticatedPost(this.endpoint(LICENSE), JSON.stringify({ license: trim(jwtToken) }))
|
||||
.then(response => helpers.parseLicense(response.license));
|
||||
},
|
||||
|
||||
load: () => {
|
||||
const { LICENSE } = endpoints;
|
||||
|
||||
return this.authenticatedGet(this.endpoint(LICENSE))
|
||||
.then(response => helpers.parseLicense(response.license));
|
||||
},
|
||||
}
|
||||
|
||||
queries = {
|
||||
run: ({ query, selected }) => {
|
||||
const { RUN_QUERY } = endpoints;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,22 @@ import nock from 'nock';
|
|||
import Kolide from 'kolide';
|
||||
import helpers from 'kolide/helpers';
|
||||
import mocks from 'test/mocks';
|
||||
import { configOptionStub, hostStub, packStub, queryStub, userStub, labelStub } from 'test/stubs';
|
||||
import {
|
||||
configOptionStub,
|
||||
hostStub,
|
||||
labelStub,
|
||||
licenseStub,
|
||||
packStub,
|
||||
queryStub,
|
||||
userStub,
|
||||
} from 'test/stubs';
|
||||
|
||||
const {
|
||||
invalidForgotPasswordRequest,
|
||||
invalidResetPasswordRequest,
|
||||
validChangePasswordRequest,
|
||||
validCreateLabelRequest,
|
||||
validCreateLicenseRequest,
|
||||
validCreatePackRequest,
|
||||
validCreateQueryRequest,
|
||||
validCreateScheduledQueryRequest,
|
||||
|
|
@ -25,6 +34,7 @@ const {
|
|||
validGetConfigRequest,
|
||||
validGetHostsRequest,
|
||||
validGetInvitesRequest,
|
||||
validGetLicenseRequest,
|
||||
validGetQueriesRequest,
|
||||
validGetQueryRequest,
|
||||
validGetScheduledQueriesRequest,
|
||||
|
|
@ -38,6 +48,7 @@ const {
|
|||
validRevokeInviteRequest,
|
||||
validRunQueryRequest,
|
||||
validSetupRequest,
|
||||
validSetupLicenseRequest,
|
||||
validStatusLabelsGetCountsRequest,
|
||||
validUpdateAdminRequest,
|
||||
validUpdateConfigOptionsRequest,
|
||||
|
|
@ -121,6 +132,119 @@ describe('Kolide - API client', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('license', () => {
|
||||
const validLicense = licenseStub();
|
||||
|
||||
describe('#create', () => {
|
||||
it('calls the correct endpoint with the correct parameters', (done) => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
const request = validCreateLicenseRequest(bearerToken, jwtToken);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.create(jwtToken)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('changes 0 allowed_hosts to Unlimited', (done) => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
const unlimitedHosts = { ...validLicense, allowed_hosts: 0 };
|
||||
|
||||
validCreateLicenseRequest(bearerToken, jwtToken, unlimitedHosts);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.create(jwtToken)
|
||||
.then((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#load', () => {
|
||||
it('calls the correct endpoint with the correct parameters', (done) => {
|
||||
const request = validGetLicenseRequest(bearerToken);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.load()
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('changes 0 allowed_hosts to Unlimited', (done) => {
|
||||
const unlimitedHosts = { ...validLicense, allowed_hosts: 0 };
|
||||
|
||||
validGetLicenseRequest(bearerToken, unlimitedHosts);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.load()
|
||||
.then((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
it('calls the correct endpoint with the correct parameters', (done) => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
const request = validSetupLicenseRequest(bearerToken, jwtToken);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.setup(jwtToken)
|
||||
.then(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(request.isDone()).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('changes 0 allowed_hosts to Unlimited', (done) => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
const unlimitedHosts = { ...validLicense, allowed_hosts: 0 };
|
||||
|
||||
validSetupLicenseRequest(bearerToken, jwtToken, unlimitedHosts);
|
||||
|
||||
Kolide.setBearerToken(bearerToken);
|
||||
Kolide.license.setup(jwtToken)
|
||||
.then((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
|
||||
done();
|
||||
})
|
||||
.catch((response) => {
|
||||
expect(response.allowed_hosts).toEqual('Unlimited', 'Expected there to be unlimited allowed hosts');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('configOptions', () => {
|
||||
it('#loadAll', (done) => {
|
||||
const request = validGetConfigOptionsRequest(bearerToken);
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ import { logoutUser } from 'redux/nodes/auth/actions';
|
|||
import { push } from 'react-router-redux';
|
||||
|
||||
import configInterface from 'interfaces/config';
|
||||
import FlashMessage from 'components/FlashMessage';
|
||||
import FlashMessage from 'components/flash_messages/FlashMessage';
|
||||
import PersistentFlash from 'components/flash_messages/PersistentFlash';
|
||||
import SiteNavHeader from 'components/side_panels/SiteNavHeader';
|
||||
import SiteNavSidePanel from 'components/side_panels/SiteNavSidePanel';
|
||||
import userInterface from 'interfaces/user';
|
||||
|
|
@ -20,6 +21,10 @@ export class CoreLayout extends Component {
|
|||
user: userInterface,
|
||||
fullWidthFlash: PropTypes.bool,
|
||||
notifications: notificationInterface,
|
||||
persistentFlash: PropTypes.shape({
|
||||
showFlash: PropTypes.bool.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
onLogoutUser = () => {
|
||||
|
|
@ -70,7 +75,7 @@ export class CoreLayout extends Component {
|
|||
}
|
||||
|
||||
render () {
|
||||
const { fullWidthFlash, notifications, children, config, user } = this.props;
|
||||
const { fullWidthFlash, notifications, children, config, persistentFlash, user } = this.props;
|
||||
const { onRemoveFlash, onUndoActionClick } = this;
|
||||
|
||||
if (!user) return false;
|
||||
|
|
@ -96,6 +101,7 @@ export class CoreLayout extends Component {
|
|||
/>
|
||||
</nav>
|
||||
<div className="core-wrapper">
|
||||
{persistentFlash.showFlash && <PersistentFlash message={persistentFlash.message} />}
|
||||
<FlashMessage
|
||||
fullWidth={fullWidthFlash}
|
||||
notification={notifications}
|
||||
|
|
@ -114,14 +120,16 @@ const mapStateToProps = (state) => {
|
|||
app: { config },
|
||||
auth: { user },
|
||||
notifications,
|
||||
persistentFlash,
|
||||
} = state;
|
||||
|
||||
const fullWidthFlash = !user;
|
||||
|
||||
return {
|
||||
notifications,
|
||||
fullWidthFlash,
|
||||
config,
|
||||
fullWidthFlash,
|
||||
notifications,
|
||||
persistentFlash,
|
||||
user,
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@ const {
|
|||
} = helpers;
|
||||
|
||||
describe('CoreLayout - layouts', () => {
|
||||
const store = { app: { config: {} }, auth: { user: userStub }, notifications: {} };
|
||||
const store = {
|
||||
app: { config: {} },
|
||||
auth: { user: userStub },
|
||||
notifications: {},
|
||||
persistentFlash: {
|
||||
showFlash: false,
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
it('renders the FlashMessage component when notifications are present', () => {
|
||||
|
|
@ -25,6 +33,10 @@ describe('CoreLayout - layouts', () => {
|
|||
isVisible: true,
|
||||
message: 'nice jerb!',
|
||||
},
|
||||
persistentFlash: {
|
||||
showFlash: false,
|
||||
message: '',
|
||||
},
|
||||
};
|
||||
const mockStoreWithNotifications = reduxMockStore(storeWithNotifications);
|
||||
const componentWithFlash = connectedComponent(CoreLayout, {
|
||||
|
|
@ -43,4 +55,23 @@ describe('CoreLayout - layouts', () => {
|
|||
expect(appWithFlash.find('FlashMessage').html()).toExist();
|
||||
expect(appWithoutFlash.find('FlashMessage').html()).toNotExist();
|
||||
});
|
||||
|
||||
it('renders the PersistentFlash component when showFlash is true', () => {
|
||||
const storeWithPersistentFlash = {
|
||||
...store,
|
||||
persistentFlash: {
|
||||
showFlash: true,
|
||||
message: 'This is the flash message',
|
||||
},
|
||||
};
|
||||
|
||||
const mockStoreWithPersistentFlash = reduxMockStore(storeWithPersistentFlash);
|
||||
|
||||
const Layout = connectedComponent(CoreLayout, {
|
||||
mockStore: mockStoreWithPersistentFlash,
|
||||
});
|
||||
const MountedLayout = mount(Layout);
|
||||
|
||||
expect(MountedLayout.find('PersistentFlash').length).toEqual(1, 'Expected the Persistent Flash to be on the page');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { size } from 'lodash';
|
||||
import { omit, size } from 'lodash';
|
||||
|
||||
import AppConfigForm from 'components/forms/admin/AppConfigForm';
|
||||
import configInterface from 'interfaces/config';
|
||||
import { createLicense, getLicense } from 'redux/nodes/auth/actions';
|
||||
import deepDifference from 'utilities/deep_difference';
|
||||
import licenseInterface from 'interfaces/license';
|
||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
||||
import SmtpWarning from 'components/SmtpWarning';
|
||||
import { updateConfig } from 'redux/nodes/app/actions';
|
||||
|
|
@ -16,6 +18,8 @@ class AppSettingsPage extends Component {
|
|||
appConfig: configInterface,
|
||||
dispatch: PropTypes.func.isRequired,
|
||||
error: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
license: licenseInterface,
|
||||
loadingLicense: PropTypes.bool,
|
||||
};
|
||||
|
||||
constructor (props) {
|
||||
|
|
@ -24,6 +28,14 @@ class AppSettingsPage extends Component {
|
|||
this.state = { showSmtpWarning: true };
|
||||
}
|
||||
|
||||
componentWillMount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(getLicense());
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
onDismissSmtpWarning = () => {
|
||||
this.setState({ showSmtpWarning: false });
|
||||
|
||||
|
|
@ -32,7 +44,8 @@ class AppSettingsPage extends Component {
|
|||
|
||||
onFormSubmit = (formData) => {
|
||||
const { appConfig, dispatch } = this.props;
|
||||
const diff = deepDifference(formData, appConfig);
|
||||
const appConfigFormData = omit(formData, ['license']);
|
||||
const diff = deepDifference(appConfigFormData, appConfig);
|
||||
|
||||
dispatch(updateConfig(diff))
|
||||
.then(() => {
|
||||
|
|
@ -51,17 +64,37 @@ class AppSettingsPage extends Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onUpdateLicense = (license) => {
|
||||
const { dispatch, license: licenseProp } = this.props;
|
||||
|
||||
if (license === licenseProp.token) {
|
||||
return false;
|
||||
}
|
||||
|
||||
dispatch(createLicense({ license }))
|
||||
.then(() => {
|
||||
dispatch(renderFlash('success', 'License updated!'));
|
||||
|
||||
return false;
|
||||
})
|
||||
.catch(() => false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { appConfig, error } = this.props;
|
||||
const { onDismissSmtpWarning, onFormSubmit } = this;
|
||||
const { appConfig, error, license, loadingLicense } = this.props;
|
||||
const { onDismissSmtpWarning, onFormSubmit, onUpdateLicense } = this;
|
||||
const { showSmtpWarning } = this.state;
|
||||
const { configured: smtpConfigured } = appConfig;
|
||||
const shouldShowWarning = !smtpConfigured && showSmtpWarning;
|
||||
|
||||
if (!size(appConfig)) {
|
||||
if (!size(appConfig) || loadingLicense) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const formData = { ...appConfig, license: license.token };
|
||||
|
||||
return (
|
||||
<div className={`${baseClass} body-wrap`}>
|
||||
<h1>App Settings</h1>
|
||||
|
|
@ -70,9 +103,11 @@ class AppSettingsPage extends Component {
|
|||
shouldShowWarning={shouldShowWarning}
|
||||
/>
|
||||
<AppConfigForm
|
||||
formData={appConfig}
|
||||
serverErrors={error}
|
||||
formData={formData}
|
||||
handleSubmit={onFormSubmit}
|
||||
handleUpdateLicense={onUpdateLicense}
|
||||
license={license}
|
||||
serverErrors={error}
|
||||
smtpConfigured={smtpConfigured}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -80,10 +115,11 @@ class AppSettingsPage extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = ({ app }) => {
|
||||
const mapStateToProps = ({ app, auth }) => {
|
||||
const { config: appConfig, error } = app;
|
||||
const { license, loading: loadingLicense } = auth;
|
||||
|
||||
return { appConfig, error };
|
||||
return { appConfig, error, license, loadingLicense };
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(AppSettingsPage);
|
||||
|
|
|
|||
|
|
@ -2,21 +2,26 @@ import expect from 'expect';
|
|||
import { mount } from 'enzyme';
|
||||
|
||||
import AppSettingsPage from 'pages/Admin/AppSettingsPage';
|
||||
import { flatConfigStub, licenseStub } from 'test/stubs';
|
||||
import testHelpers from 'test/helpers';
|
||||
|
||||
const { connectedComponent, reduxMockStore } = testHelpers;
|
||||
const baseStore = {
|
||||
app: { config: flatConfigStub },
|
||||
auth: { license: licenseStub },
|
||||
};
|
||||
const storeWithoutSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: false } } };
|
||||
const storeWithSMTPConfig = { ...baseStore, app: { config: { ...flatConfigStub, configured: true } } };
|
||||
|
||||
describe('AppSettingsPage - component', () => {
|
||||
it('renders', () => {
|
||||
const mockStore = reduxMockStore({ app: { config: {} } });
|
||||
const mockStore = reduxMockStore(baseStore);
|
||||
const page = mount(connectedComponent(AppSettingsPage, { mockStore }));
|
||||
|
||||
expect(page.find('AppSettingsPage').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('renders a warning if SMTP has not been configured', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: false } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
|
|
@ -30,8 +35,6 @@ describe('AppSettingsPage - component', () => {
|
|||
});
|
||||
|
||||
it('dismisses the smtp warning when "DISMISS" is clicked', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: false } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
|
|
@ -46,9 +49,7 @@ describe('AppSettingsPage - component', () => {
|
|||
});
|
||||
|
||||
it('does not render a warning if SMTP has been configured', () => {
|
||||
const storeWithoutSMTPConfig = { app: { config: { configured: true } } };
|
||||
|
||||
const mockStore = reduxMockStore(storeWithoutSMTPConfig);
|
||||
const mockStore = reduxMockStore(storeWithSMTPConfig);
|
||||
const page = mount(
|
||||
connectedComponent(AppSettingsPage, { mockStore })
|
||||
).find('AppSettingsPage');
|
||||
|
|
|
|||
82
frontend/pages/LicensePage/LicensePage.jsx
Normal file
82
frontend/pages/LicensePage/LicensePage.jsx
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
import React, { Component, PropTypes } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { setupLicense } from 'redux/nodes/auth/actions';
|
||||
import EnsureUnauthenticated from 'components/EnsureUnauthenticated';
|
||||
import Footer from 'components/Footer';
|
||||
import LicenseForm from 'components/forms/LicenseForm';
|
||||
import licenseInterface from 'interfaces/license';
|
||||
import LicenseSuccess from 'components/LicenseSuccess';
|
||||
import { showBackgroundImage } from 'redux/nodes/app/actions';
|
||||
|
||||
import kolideLogo from '../../../assets/images/kolide-logo-condensed.svg';
|
||||
|
||||
const baseClass = 'license-page';
|
||||
|
||||
class LicensePage extends Component {
|
||||
static propTypes = {
|
||||
dispatch: PropTypes.func,
|
||||
errors: PropTypes.shape({
|
||||
base: PropTypes.string,
|
||||
license: PropTypes.string,
|
||||
}),
|
||||
license: licenseInterface.isRequired,
|
||||
};
|
||||
|
||||
componentWillMount () {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(showBackgroundImage);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
handleSubmit = ({ license }) => {
|
||||
const { dispatch } = this.props;
|
||||
|
||||
dispatch(setupLicense({ license }))
|
||||
.catch(() => false);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
render () {
|
||||
const { handleSubmit } = this;
|
||||
const { errors, license } = this.props;
|
||||
|
||||
if (license.token) {
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<img
|
||||
alt="Kolide"
|
||||
src={kolideLogo}
|
||||
className={`${baseClass}__logo`}
|
||||
/>
|
||||
<LicenseSuccess license={license} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={baseClass}>
|
||||
<img
|
||||
alt="Kolide"
|
||||
src={kolideLogo}
|
||||
className={`${baseClass}__logo`}
|
||||
/>
|
||||
<LicenseForm handleSubmit={handleSubmit} serverErrors={errors} />
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
const { errors, license } = state.auth;
|
||||
|
||||
return { errors, license };
|
||||
};
|
||||
|
||||
const ConnectedComponent = connect(mapStateToProps)(LicensePage);
|
||||
export default EnsureUnauthenticated(ConnectedComponent);
|
||||
123
frontend/pages/LicensePage/LicensePage.tests.jsx
Normal file
123
frontend/pages/LicensePage/LicensePage.tests.jsx
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
import expect, { spyOn, restoreSpies } from 'expect';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import helpers from 'test/helpers';
|
||||
import Kolide from 'kolide';
|
||||
import LicensePage from 'pages/LicensePage';
|
||||
import stubs from 'test/stubs';
|
||||
|
||||
const {
|
||||
connectedComponent,
|
||||
reduxMockStore,
|
||||
} = helpers;
|
||||
const {
|
||||
licenseStub,
|
||||
userStub,
|
||||
} = stubs;
|
||||
|
||||
describe('LicensePage - component', () => {
|
||||
describe('rendering', () => {
|
||||
it('renders the license success content when a license is present', () => {
|
||||
const store = {
|
||||
auth: {
|
||||
license: licenseStub(),
|
||||
loading: false,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
|
||||
expect(mount(Component).find('LicenseForm').length).toEqual(0, 'Expected the LicenseForm to not be on the page when a license is present');
|
||||
expect(mount(Component).find('LicenseSuccess').length).toEqual(1, 'Expected the LicenseSuccess component to be on the page when a license is present');
|
||||
});
|
||||
|
||||
it('renders when not authenticated', () => {
|
||||
const store = {
|
||||
auth: {
|
||||
license: {},
|
||||
loading: false,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
|
||||
expect(mount(Component).find('LicensePage').length).toEqual(1);
|
||||
});
|
||||
|
||||
it('does not render when a user is logged in', () => {
|
||||
const store = {
|
||||
auth: {
|
||||
license: {},
|
||||
loading: false,
|
||||
user: userStub,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
|
||||
expect(mount(Component).find('LicensePage').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('does not render when loading the user', () => {
|
||||
const store = {
|
||||
auth: {
|
||||
license: {},
|
||||
loading: true,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
|
||||
expect(mount(Component).find('LicensePage').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('renders a LicenseForm when a license is not present', () => {
|
||||
const store = {
|
||||
auth: {
|
||||
license: {},
|
||||
loading: false,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
|
||||
expect(mount(Component).find('LicenseForm').length).toEqual(1, 'Expected the LicenseForm to be on the page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('submitting the form', () => {
|
||||
afterEach(restoreSpies);
|
||||
|
||||
it('calls the Kolide setup license endpoint', () => {
|
||||
spyOn(Kolide.license, 'setup').andReturn(Promise.resolve());
|
||||
|
||||
const store = {
|
||||
auth: {
|
||||
license: {},
|
||||
loading: false,
|
||||
user: null,
|
||||
},
|
||||
};
|
||||
const Component = connectedComponent(LicensePage, {
|
||||
mockStore: reduxMockStore(store),
|
||||
});
|
||||
const Form = mount(Component).find('LicenseForm');
|
||||
const LicenseField = Form.find({ name: 'license' }).find('textarea');
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
|
||||
helpers.fillInFormInput(LicenseField, jwtToken);
|
||||
Form.simulate('submit');
|
||||
|
||||
expect(Kolide.license.setup).toHaveBeenCalledWith(jwtToken);
|
||||
});
|
||||
});
|
||||
});
|
||||
20
frontend/pages/LicensePage/_styles.scss
Normal file
20
frontend/pages/LicensePage/_styles.scss
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
.license-page {
|
||||
@include display(flex);
|
||||
@include justify-content(center);
|
||||
@include flex-direction(column);
|
||||
min-height: calc(100vh - #{$footer-height});
|
||||
position: relative;
|
||||
|
||||
&__logo {
|
||||
@include position(absolute, 15px null null 15px);
|
||||
width: 200px;
|
||||
|
||||
@include breakpoint(tablet) {
|
||||
width: 125px;
|
||||
position: static;
|
||||
display: block;
|
||||
margin: 15px 0 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
frontend/pages/LicensePage/index.js
Normal file
1
frontend/pages/LicensePage/index.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export default from './LicensePage';
|
||||
12
frontend/redux/middlewares/nag_message/helpers.js
Normal file
12
frontend/redux/middlewares/nag_message/helpers.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import moment from 'moment';
|
||||
|
||||
const shouldNagUser = ({ license }) => {
|
||||
const { allowed_hosts: allowedHosts, expiry, hosts, revoked } = license;
|
||||
|
||||
const hostsOverenrolled = hosts > allowedHosts;
|
||||
const licenseExpired = moment().isAfter(moment(expiry));
|
||||
|
||||
return hostsOverenrolled || licenseExpired || revoked;
|
||||
};
|
||||
|
||||
export default { shouldNagUser };
|
||||
41
frontend/redux/middlewares/nag_message/helpers.tests.js
Normal file
41
frontend/redux/middlewares/nag_message/helpers.tests.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import helpers from 'redux/middlewares/nag_message/helpers';
|
||||
import { licenseStub } from 'test/stubs';
|
||||
|
||||
const validLicense = licenseStub();
|
||||
|
||||
describe('Nag message middleware - helpers', () => {
|
||||
describe('#shouldNagUser', () => {
|
||||
const { shouldNagUser } = helpers;
|
||||
|
||||
it('returns true when there are more hosts than allowed hosts', () => {
|
||||
const overusedLicense = {
|
||||
...validLicense,
|
||||
allowed_hosts: 2,
|
||||
hosts: 3,
|
||||
};
|
||||
|
||||
expect(shouldNagUser({ license: overusedLicense })).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns true when the license is expired', () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const expiredLicense = { ...validLicense, expiry: yesterday.toISOString() };
|
||||
|
||||
expect(shouldNagUser({ license: expiredLicense })).toEqual(true, 'Expected the expired license to return true');
|
||||
});
|
||||
|
||||
it('returns true when the license is revoked', () => {
|
||||
const revokedLicense = { ...validLicense, revoked: true };
|
||||
|
||||
expect(shouldNagUser({ license: revokedLicense })).toEqual(true, 'Expected the revoked license to return true');
|
||||
});
|
||||
|
||||
it('returns false when the license is valid', () => {
|
||||
expect(shouldNagUser({ license: validLicense })).toEqual(false, 'Expected the valid license to return false');
|
||||
});
|
||||
});
|
||||
});
|
||||
20
frontend/redux/middlewares/nag_message/index.js
Normal file
20
frontend/redux/middlewares/nag_message/index.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
import actions from 'redux/nodes/persistent_flash/actions';
|
||||
import helpers from 'redux/middlewares/nag_message/helpers';
|
||||
import { LICENSE_FAILURE, LICENSE_SUCCESS } from 'redux/nodes/auth/actions';
|
||||
|
||||
const nagMessageMiddleware = store => next => (action) => {
|
||||
const { type, payload } = action;
|
||||
|
||||
if (type === LICENSE_SUCCESS) {
|
||||
if (helpers.shouldNagUser(payload)) {
|
||||
store.dispatch(actions.showPersistentFlash('Please upgrade your Kolide license'));
|
||||
} else {
|
||||
store.dispatch(actions.hidePersistentFlash);
|
||||
}
|
||||
}
|
||||
|
||||
return next(action);
|
||||
};
|
||||
|
||||
export default nagMessageMiddleware;
|
||||
69
frontend/redux/middlewares/nag_message/nag_message.tests.js
Normal file
69
frontend/redux/middlewares/nag_message/nag_message.tests.js
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import { licenseSuccess } from 'redux/nodes/auth/actions';
|
||||
import { licenseStub } from 'test/stubs';
|
||||
import { reduxMockStore } from 'test/helpers';
|
||||
|
||||
const validLicense = licenseStub();
|
||||
|
||||
describe('nag_message - middleware', () => {
|
||||
it('dispatches a persistent flash message when a license is overused', () => {
|
||||
const mockStore = reduxMockStore();
|
||||
const overusedLicense = {
|
||||
...validLicense,
|
||||
allowed_hosts: 2,
|
||||
hosts: 3,
|
||||
};
|
||||
const expectedNagMessageAction = {
|
||||
type: 'SHOW_PERSISTENT_FLASH',
|
||||
payload: {
|
||||
message: 'Please upgrade your Kolide license',
|
||||
},
|
||||
};
|
||||
|
||||
mockStore.dispatch(licenseSuccess(overusedLicense));
|
||||
expect(mockStore.getActions()).toInclude(expectedNagMessageAction);
|
||||
});
|
||||
|
||||
it('dispatches a persistent flash message when a license is expired', () => {
|
||||
const yesterday = new Date();
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
const mockStore = reduxMockStore();
|
||||
|
||||
const expiredLicense = { ...validLicense, expiry: yesterday.toISOString() };
|
||||
const expectedNagMessageAction = {
|
||||
type: 'SHOW_PERSISTENT_FLASH',
|
||||
payload: {
|
||||
message: 'Please upgrade your Kolide license',
|
||||
},
|
||||
};
|
||||
|
||||
mockStore.dispatch(licenseSuccess(expiredLicense));
|
||||
expect(mockStore.getActions()).toInclude(expectedNagMessageAction);
|
||||
});
|
||||
|
||||
it('dispatches a persistent flash message when a license is revoked', () => {
|
||||
const mockStore = reduxMockStore();
|
||||
|
||||
const revokedLicense = { ...validLicense, revoked: true };
|
||||
const expectedNagMessageAction = {
|
||||
type: 'SHOW_PERSISTENT_FLASH',
|
||||
payload: {
|
||||
message: 'Please upgrade your Kolide license',
|
||||
},
|
||||
};
|
||||
|
||||
mockStore.dispatch(licenseSuccess(revokedLicense));
|
||||
expect(mockStore.getActions()).toInclude(expectedNagMessageAction);
|
||||
});
|
||||
|
||||
it('dispatches an action to clear persistent flash message when the license is valid', () => {
|
||||
const mockStore = reduxMockStore();
|
||||
const expectedNagMessageAction = { type: 'HIDE_PERSISTENT_FLASH' };
|
||||
|
||||
mockStore.dispatch(licenseSuccess(validLicense));
|
||||
expect(mockStore.getActions()).toInclude(expectedNagMessageAction);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -4,6 +4,9 @@ import Kolide from 'kolide';
|
|||
import userActions from 'redux/nodes/entities/users/actions';
|
||||
|
||||
export const CLEAR_AUTH_ERRORS = 'CLEAR_AUTH_ERRORS';
|
||||
export const LICENSE_REQUEST = 'LICENSE_REQUEST';
|
||||
export const LICENSE_SUCCESS = 'LICENSE_SUCCESS';
|
||||
export const LICENSE_FAILURE = 'LICENSE_FAILURE';
|
||||
export const LOGIN_REQUEST = 'LOGIN_REQUEST';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAILURE = 'LOGIN_FAILURE';
|
||||
|
|
@ -17,6 +20,74 @@ export const PERFORM_REQUIRED_PASSWORD_RESET_REQUEST = 'PERFORM_REQUIRED_PASSWOR
|
|||
export const PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS = 'PERFORM_REQUIRED_PASSWORD_RESET_SUCCESS';
|
||||
export const PERFORM_REQUIRED_PASSWORD_RESET_FAILURE = 'PERFORM_REQUIRED_PASSWORD_RESET_FAILURE';
|
||||
|
||||
export const licenseFailure = (errors) => {
|
||||
return {
|
||||
type: LICENSE_FAILURE,
|
||||
payload: { errors },
|
||||
};
|
||||
};
|
||||
export const licenseRequest = { type: LICENSE_REQUEST };
|
||||
export const licenseSuccess = (license) => {
|
||||
return {
|
||||
type: LICENSE_SUCCESS,
|
||||
payload: { license },
|
||||
};
|
||||
};
|
||||
export const setupLicense = ({ license }) => {
|
||||
return (dispatch) => {
|
||||
dispatch(licenseRequest);
|
||||
|
||||
return Kolide.license.setup(license)
|
||||
.then((response) => {
|
||||
dispatch(licenseSuccess(response));
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch((response) => {
|
||||
const errorsObject = formatErrorResponse(response);
|
||||
dispatch(licenseFailure(errorsObject));
|
||||
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
export const createLicense = ({ license }) => {
|
||||
return (dispatch) => {
|
||||
dispatch(licenseRequest);
|
||||
|
||||
return Kolide.license.create(license)
|
||||
.then((response) => {
|
||||
dispatch(licenseSuccess(response));
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch((response) => {
|
||||
const errorsObject = formatErrorResponse(response);
|
||||
dispatch(licenseFailure(errorsObject));
|
||||
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
export const getLicense = () => {
|
||||
return (dispatch) => {
|
||||
dispatch(licenseRequest);
|
||||
|
||||
return Kolide.license.load()
|
||||
.then((license) => {
|
||||
dispatch(licenseSuccess(license));
|
||||
|
||||
return license;
|
||||
})
|
||||
.catch((response) => {
|
||||
const errorsObject = formatErrorResponse(response);
|
||||
dispatch(licenseFailure(errorsObject));
|
||||
|
||||
throw response;
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
export const clearAuthErrors = { type: CLEAR_AUTH_ERRORS };
|
||||
export const loginRequest = { type: LOGIN_REQUEST };
|
||||
export const loginSuccess = ({ user, token }) => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,12 @@ import { reduxMockStore } from 'test/helpers';
|
|||
import { userStub } from 'test/stubs';
|
||||
|
||||
import {
|
||||
createLicense,
|
||||
getLicense,
|
||||
setupLicense,
|
||||
LICENSE_FAILURE,
|
||||
LICENSE_REQUEST,
|
||||
LICENSE_SUCCESS,
|
||||
performRequiredPasswordReset,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_REQUEST,
|
||||
PERFORM_REQUIRED_PASSWORD_RESET_FAILURE,
|
||||
|
|
@ -18,6 +24,288 @@ const store = { entities: { invites: {}, users: {} } };
|
|||
const user = { ...userStub, id: 1, email: 'zwass@kolide.co', force_password_reset: false };
|
||||
|
||||
describe('Auth - actions', () => {
|
||||
describe('#createLicense', () => {
|
||||
const license = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'create').andReturn(Promise.resolve({ license }));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(createLicense({ license }))
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.create).toHaveBeenCalledWith(license);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.create).toHaveBeenCalledWith(license);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{ type: 'HIDE_PERSISTENT_FLASH' },
|
||||
{
|
||||
type: LICENSE_SUCCESS,
|
||||
payload: { license: { license } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(createLicense({ license }))
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessful request', () => {
|
||||
const errors = [
|
||||
{
|
||||
name: 'base',
|
||||
reason: 'Unable to create license',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to create license',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'create').andReturn(Promise.reject(errorResponse));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(createLicense({ license }))
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.create).toHaveBeenCalledWith(license);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.create).toHaveBeenCalledWith(license);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{
|
||||
type: LICENSE_FAILURE,
|
||||
payload: { errors: { base: 'Unable to create license', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(createLicense({ license }))
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setupLicense', () => {
|
||||
const license = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'setup').andReturn(Promise.resolve({ license }));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(setupLicense({ license }))
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.setup).toHaveBeenCalledWith(license);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.setup).toHaveBeenCalledWith(license);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{ type: 'HIDE_PERSISTENT_FLASH' },
|
||||
{
|
||||
type: LICENSE_SUCCESS,
|
||||
payload: { license: { license } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(setupLicense({ license }))
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessful request', () => {
|
||||
const errors = [
|
||||
{
|
||||
name: 'base',
|
||||
reason: 'Unable to create license',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to create license',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'setup').andReturn(Promise.reject(errorResponse));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(setupLicense({ license }))
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.setup).toHaveBeenCalledWith(license);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.create).toHaveBeenCalledWith(license);
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{
|
||||
type: LICENSE_FAILURE,
|
||||
payload: { errors: { base: 'Unable to create license', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(setupLicense({ license }))
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getLicense', () => {
|
||||
const license = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
afterEach(restoreSpies);
|
||||
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'load').andReturn(Promise.resolve({ license }));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(getLicense())
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.load).toHaveBeenCalled();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.load).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{ type: 'HIDE_PERSISTENT_FLASH' },
|
||||
{
|
||||
type: LICENSE_SUCCESS,
|
||||
payload: { license: { license } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(getLicense())
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessful request', () => {
|
||||
const errors = [
|
||||
{
|
||||
name: 'base',
|
||||
reason: 'Unable to get license',
|
||||
},
|
||||
];
|
||||
const errorResponse = {
|
||||
status: 422,
|
||||
message: {
|
||||
message: 'Unable to get license',
|
||||
errors,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(Kolide.default.license, 'load').andReturn(Promise.reject(errorResponse));
|
||||
});
|
||||
|
||||
it('calls the API', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
mockStore.dispatch(getLicense())
|
||||
.then(() => {
|
||||
expect(Kolide.default.license.load).toHaveBeenCalled();
|
||||
})
|
||||
.catch(() => {
|
||||
expect(Kolide.default.license.load).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
const expectedActions = [
|
||||
{ type: LICENSE_REQUEST },
|
||||
{
|
||||
type: LICENSE_FAILURE,
|
||||
payload: { errors: { base: 'Unable to get license', http_status: 422 } },
|
||||
},
|
||||
];
|
||||
|
||||
return mockStore.dispatch(getLicense())
|
||||
.then(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
})
|
||||
.catch(() => {
|
||||
expect(mockStore.getActions()).toEqual(expectedActions);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispatching the perform required password reset action', () => {
|
||||
describe('successful request', () => {
|
||||
beforeEach(() => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import {
|
||||
CLEAR_AUTH_ERRORS,
|
||||
LICENSE_FAILURE,
|
||||
LICENSE_REQUEST,
|
||||
LICENSE_SUCCESS,
|
||||
LOGIN_FAILURE,
|
||||
LOGIN_REQUEST,
|
||||
LOGIN_SUCCESS,
|
||||
|
|
@ -15,6 +18,7 @@ import {
|
|||
} from './actions';
|
||||
|
||||
export const initialState = {
|
||||
license: {},
|
||||
loading: false,
|
||||
errors: {},
|
||||
user: null,
|
||||
|
|
@ -27,6 +31,7 @@ const reducer = (state = initialState, action) => {
|
|||
...state,
|
||||
errors: {},
|
||||
};
|
||||
case LICENSE_REQUEST:
|
||||
case LOGIN_REQUEST:
|
||||
case LOGOUT_REQUEST:
|
||||
case UPDATE_USER_REQUEST:
|
||||
|
|
@ -34,12 +39,19 @@ const reducer = (state = initialState, action) => {
|
|||
...state,
|
||||
loading: true,
|
||||
};
|
||||
case LICENSE_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
license: action.payload.license,
|
||||
};
|
||||
case LOGIN_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
user: action.payload.user,
|
||||
};
|
||||
case LICENSE_FAILURE:
|
||||
case LOGIN_FAILURE:
|
||||
return {
|
||||
...state,
|
||||
|
|
|
|||
|
|
@ -157,13 +157,15 @@ describe('reduxConfig', () => {
|
|||
const { actions, reducer } = config;
|
||||
|
||||
it('calls the createFunc', () => {
|
||||
mockStore.dispatch(actions.create());
|
||||
mockStore.dispatch(actions.create())
|
||||
.catch(() => false);
|
||||
|
||||
expect(createFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.create());
|
||||
mockStore.dispatch(actions.create())
|
||||
.catch(() => false);
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
|
@ -344,9 +346,8 @@ describe('reduxConfig', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('unprocessable entitiy', () => {
|
||||
describe('unprocessable entity', () => {
|
||||
const mockStore = reduxMockStore(store);
|
||||
|
||||
const errors = [
|
||||
{ name: 'first_name',
|
||||
reason: 'is not valid',
|
||||
|
|
@ -374,7 +375,8 @@ describe('reduxConfig', () => {
|
|||
const { actions, reducer } = config;
|
||||
|
||||
it('calls the updateFunc', () => {
|
||||
mockStore.dispatch(actions.update(user));
|
||||
mockStore.dispatch(actions.update(user))
|
||||
.catch(() => false);
|
||||
|
||||
expect(updateFunc).toHaveBeenCalledWith(user);
|
||||
});
|
||||
|
|
@ -573,13 +575,15 @@ describe('reduxConfig', () => {
|
|||
const { actions, reducer } = config;
|
||||
|
||||
it('calls the createFunc', () => {
|
||||
mockStore.dispatch(actions.destroy());
|
||||
mockStore.dispatch(actions.destroy())
|
||||
.catch(() => false);
|
||||
|
||||
expect(destroyFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.destroy());
|
||||
mockStore.dispatch(actions.destroy())
|
||||
.catch(() => false);
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
|
@ -762,13 +766,15 @@ describe('reduxConfig', () => {
|
|||
const { actions, reducer } = config;
|
||||
|
||||
it('calls the loadFunc', () => {
|
||||
mockStore.dispatch(actions.load());
|
||||
mockStore.dispatch(actions.load())
|
||||
.catch(() => false);
|
||||
|
||||
expect(loadFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.load());
|
||||
mockStore.dispatch(actions.load())
|
||||
.catch(() => false);
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
|
@ -880,13 +886,15 @@ describe('reduxConfig', () => {
|
|||
const { actions, reducer } = config;
|
||||
|
||||
it('calls the loadAllFunc', () => {
|
||||
mockStore.dispatch(actions.loadAll());
|
||||
mockStore.dispatch(actions.loadAll())
|
||||
.catch(() => false);
|
||||
|
||||
expect(loadAllFunc).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('dispatches the correct actions', () => {
|
||||
mockStore.dispatch(actions.loadAll());
|
||||
mockStore.dispatch(actions.loadAll())
|
||||
.catch(() => false);
|
||||
|
||||
const dispatchedActions = mockStore.getActions();
|
||||
const dispatchedActionTypes = dispatchedActions.map((action) => { return action.type; });
|
||||
|
|
|
|||
22
frontend/redux/nodes/persistent_flash/actions.js
Normal file
22
frontend/redux/nodes/persistent_flash/actions.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Action Types
|
||||
const HIDE_PERSISTENT_FLASH = 'HIDE_PERSISTENT_FLASH';
|
||||
const SHOW_PERSISTENT_FLASH = 'SHOW_PERSISTENT_FLASH';
|
||||
|
||||
export const ACTION_TYPES = {
|
||||
HIDE_PERSISTENT_FLASH,
|
||||
SHOW_PERSISTENT_FLASH,
|
||||
};
|
||||
|
||||
// Actions
|
||||
const hidePersistentFlash = { type: HIDE_PERSISTENT_FLASH };
|
||||
const showPersistentFlash = (message) => {
|
||||
return {
|
||||
type: SHOW_PERSISTENT_FLASH,
|
||||
payload: { message },
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
hidePersistentFlash,
|
||||
showPersistentFlash,
|
||||
};
|
||||
23
frontend/redux/nodes/persistent_flash/reducer.js
Normal file
23
frontend/redux/nodes/persistent_flash/reducer.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import { ACTION_TYPES } from 'redux/nodes/persistent_flash/actions';
|
||||
|
||||
export const initialState = {
|
||||
showFlash: false,
|
||||
message: '',
|
||||
};
|
||||
|
||||
export default (state = initialState, { type, payload }) => {
|
||||
switch (type) {
|
||||
case ACTION_TYPES.HIDE_PERSISTENT_FLASH:
|
||||
return {
|
||||
showFlash: false,
|
||||
message: '',
|
||||
};
|
||||
case ACTION_TYPES.SHOW_PERSISTENT_FLASH:
|
||||
return {
|
||||
showFlash: true,
|
||||
message: payload.message,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
32
frontend/redux/nodes/persistent_flash/reducer.tests.js
Normal file
32
frontend/redux/nodes/persistent_flash/reducer.tests.js
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import expect from 'expect';
|
||||
|
||||
import actions from 'redux/nodes/persistent_flash/actions';
|
||||
import reducer, { initialState } from 'redux/nodes/persistent_flash/reducer';
|
||||
|
||||
describe('persistent_flash - reducer', () => {
|
||||
it('sets the initial state', () => {
|
||||
const nextState = reducer(undefined, { type: 'SOME_ACTION' });
|
||||
|
||||
expect(nextState).toEqual(initialState);
|
||||
});
|
||||
|
||||
it('shows the flash and sets the message when showPersistentFlash is dispatched', () => {
|
||||
const message = 'This is the flash message';
|
||||
const action = actions.showPersistentFlash(message);
|
||||
|
||||
expect(reducer(initialState, action)).toEqual({
|
||||
showFlash: true,
|
||||
message,
|
||||
});
|
||||
});
|
||||
|
||||
it('hides the flash and removes the message when hidePersistentFlash is dispatched', () => {
|
||||
const currentState = { showFlash: true, message: 'something' };
|
||||
const action = actions.hidePersistentFlash;
|
||||
|
||||
expect(reducer(currentState, action)).toEqual({
|
||||
showFlash: false,
|
||||
message: '',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@ import auth from './nodes/auth/reducer';
|
|||
import components from './nodes/components/reducer';
|
||||
import entities from './nodes/entities/reducer';
|
||||
import notifications from './nodes/notifications/reducer';
|
||||
import persistentFlash from './nodes/persistent_flash/reducer';
|
||||
import redirectLocation from './nodes/redirectLocation/reducer';
|
||||
|
||||
export default combineReducers({
|
||||
|
|
@ -16,6 +17,7 @@ export default combineReducers({
|
|||
entities,
|
||||
loadingBar: loadingBarReducer,
|
||||
notifications,
|
||||
persistentFlash,
|
||||
redirectLocation,
|
||||
routing: routerReducer,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import { routerMiddleware } from 'react-router-redux';
|
|||
import thunkMiddleware from 'redux-thunk';
|
||||
|
||||
import authMiddleware from './middlewares/auth';
|
||||
import nagMessageMiddleware from './middlewares/nag_message';
|
||||
import redirectMiddleware from './middlewares/redirect';
|
||||
import reducers from './reducers';
|
||||
|
||||
|
|
@ -14,6 +15,7 @@ const appliedMiddleware = applyMiddleware(
|
|||
thunkMiddleware,
|
||||
routerMiddleware(browserHistory),
|
||||
authMiddleware,
|
||||
nagMessageMiddleware,
|
||||
redirectMiddleware,
|
||||
loadingBarMiddleware({
|
||||
promiseTypeSuffixes: ['REQUEST', 'SUCCESS', 'FAILURE'],
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import ConfigOptionsPage from 'pages/config/ConfigOptionsPage';
|
|||
import ConfirmInvitePage from 'pages/ConfirmInvitePage';
|
||||
import CoreLayout from 'layouts/CoreLayout';
|
||||
import EditPackPage from 'pages/packs/EditPackPage';
|
||||
import LicensePage from 'pages/LicensePage';
|
||||
import LoginRoutes from 'components/LoginRoutes';
|
||||
import LogoutPage from 'pages/LogoutPage';
|
||||
import ManageHostsPage from 'pages/hosts/ManageHostsPage';
|
||||
|
|
@ -34,6 +35,7 @@ const routes = (
|
|||
<Router history={history}>
|
||||
<Route path="/" component={App}>
|
||||
<Route path="setup" component={RegistrationPage} />
|
||||
<Route path="license" component={LicensePage} />
|
||||
<Route path="login" component={LoginRoutes}>
|
||||
<Route path="invites/:invite_token" component={ConfirmInvitePage} />
|
||||
<Route path="forgot" />
|
||||
|
|
|
|||
|
|
@ -8,10 +8,12 @@ export default {
|
|||
FORGOT_PASSWORD: '/login/forgot',
|
||||
HOME: '/',
|
||||
KOLIDE_500: '/500',
|
||||
LICENSE: '/license',
|
||||
LOGIN: '/login',
|
||||
LOGOUT: '/logout',
|
||||
MANAGE_HOSTS: '/hosts/manage',
|
||||
NEW_PACK: '/packs/new',
|
||||
NEW_QUERY: '/queries/new',
|
||||
RESET_PASSWORD: '/login/reset',
|
||||
SETUP: '/setup',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Provider } from 'react-redux';
|
|||
import thunk from 'redux-thunk';
|
||||
|
||||
import authMiddleware from 'redux/middlewares/auth';
|
||||
import nagMessageMiddleware from 'redux/middlewares/nag_message';
|
||||
import redirectMiddleware from 'redux/middlewares/redirect';
|
||||
|
||||
export const fillInFormInput = (inputComponent, value) => {
|
||||
|
|
@ -13,7 +14,7 @@ export const fillInFormInput = (inputComponent, value) => {
|
|||
};
|
||||
|
||||
export const reduxMockStore = (store = {}) => {
|
||||
const middlewares = [thunk, authMiddleware, redirectMiddleware];
|
||||
const middlewares = [thunk, authMiddleware, nagMessageMiddleware, redirectMiddleware];
|
||||
const mockStore = configureStore(middlewares);
|
||||
|
||||
return mockStore(store);
|
||||
|
|
@ -45,7 +46,8 @@ export const itBehavesLikeAFormDropdownElement = (form, inputName) => {
|
|||
};
|
||||
|
||||
export const itBehavesLikeAFormInputElement = (form, inputName, inputType = 'InputField', inputText = 'some text') => {
|
||||
const inputField = form.find({ name: inputName }).find('input');
|
||||
const Input = form.find({ name: inputName });
|
||||
const inputField = inputType === 'textarea' ? Input.find('textarea') : Input.find('input');
|
||||
|
||||
expect(inputField.length).toEqual(1);
|
||||
|
||||
|
|
|
|||
|
|
@ -34,6 +34,53 @@ export const validCreateLabelRequest = (bearerToken, labelParams) => {
|
|||
.reply(201, { label: { ...labelParams, display_text: labelParams.name } });
|
||||
};
|
||||
|
||||
export const validCreateLicenseRequest = (bearerToken, jwtToken, response = stubs.licenseStub()) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
})
|
||||
.post('/api/v1/kolide/license', JSON.stringify({ license: jwtToken }))
|
||||
.reply(201, {
|
||||
license: {
|
||||
...response,
|
||||
token: jwtToken,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const validSetupLicenseRequest = (bearerToken, jwtToken, response = stubs.licenseStub()) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
})
|
||||
.post('/api/v1/license', JSON.stringify({ license: jwtToken }))
|
||||
.reply(201, {
|
||||
license: {
|
||||
...response,
|
||||
license: jwtToken,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const validGetLicenseRequest = (bearerToken, response = stubs.licenseStub()) => {
|
||||
const jwtToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ';
|
||||
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
Authorization: `Bearer ${bearerToken}`,
|
||||
},
|
||||
})
|
||||
.get('/api/v1/kolide/license')
|
||||
.reply(200, {
|
||||
license: {
|
||||
...response,
|
||||
license: jwtToken,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export const validCreatePackRequest = (bearerToken, packParams) => {
|
||||
return nock('http://localhost:8080', {
|
||||
reqHeaders: {
|
||||
|
|
@ -447,6 +494,7 @@ export default {
|
|||
invalidResetPasswordRequest,
|
||||
validChangePasswordRequest,
|
||||
validCreateLabelRequest,
|
||||
validCreateLicenseRequest,
|
||||
validCreatePackRequest,
|
||||
validCreateQueryRequest,
|
||||
validCreateScheduledQueryRequest,
|
||||
|
|
@ -461,6 +509,7 @@ export default {
|
|||
validGetConfigRequest,
|
||||
validGetHostsRequest,
|
||||
validGetInvitesRequest,
|
||||
validGetLicenseRequest,
|
||||
validGetQueriesRequest,
|
||||
validGetQueryRequest,
|
||||
validGetScheduledQueriesRequest,
|
||||
|
|
@ -474,6 +523,7 @@ export default {
|
|||
validRevokeInviteRequest,
|
||||
validRunQueryRequest,
|
||||
validSetupRequest,
|
||||
validSetupLicenseRequest,
|
||||
validStatusLabelsGetCountsRequest,
|
||||
validUpdateAdminRequest,
|
||||
validUpdateConfigOptionsRequest,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,25 @@ export const configStub = {
|
|||
},
|
||||
};
|
||||
|
||||
export const flatConfigStub = {
|
||||
org_name: 'Kolide',
|
||||
org_logo_url: '0.0.0.0:8080/logo.png',
|
||||
kolide_server_url: '',
|
||||
configured: false,
|
||||
domain: '',
|
||||
sender_address: '',
|
||||
server: '',
|
||||
port: 587,
|
||||
authentication_type: 'authtype_username_password',
|
||||
user_name: '',
|
||||
password: '',
|
||||
enable_ssl_tls: true,
|
||||
authentication_method: 'authmethod_plain',
|
||||
verify_ssl_certs: true,
|
||||
enable_start_tls: true,
|
||||
email_enabled: false,
|
||||
};
|
||||
|
||||
export const hostStub = {
|
||||
created_at: '2017-01-10T19:18:55Z',
|
||||
updated_at: '2017-01-10T20:13:52Z',
|
||||
|
|
@ -117,6 +136,21 @@ export const labelStub = {
|
|||
target_type: 'labels',
|
||||
};
|
||||
|
||||
export const licenseStub = () => {
|
||||
const tomorrow = new Date();
|
||||
tomorrow.setDate(tomorrow.getDate() + 1);
|
||||
|
||||
return {
|
||||
token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ',
|
||||
expiry: tomorrow.toISOString(),
|
||||
allowed_hosts: 100,
|
||||
hosts: 70,
|
||||
evaluation: false,
|
||||
revoked: false,
|
||||
organization: 'Kolide',
|
||||
};
|
||||
};
|
||||
|
||||
export const packStub = {
|
||||
created_at: '0001-01-01T00:00:00Z',
|
||||
updated_at: '0001-01-01T00:00:00Z',
|
||||
|
|
@ -173,8 +207,10 @@ export const userStub = {
|
|||
export default {
|
||||
adminUserStub,
|
||||
configStub,
|
||||
flatConfigStub,
|
||||
hostStub,
|
||||
labelStub,
|
||||
licenseStub,
|
||||
packStub,
|
||||
queryStub,
|
||||
scheduledQueryStub,
|
||||
|
|
|
|||
Loading…
Reference in a new issue