mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-06 01:15:28 +02:00
Merge pull request #99 from Unleash/oauth2
Add support for "builtin" and Custom (Oauth2) auhtentications
This commit is contained in:
commit
1f40dbba3c
@ -18,7 +18,7 @@ import { Link } from 'react-router';
|
|||||||
import styles from './styles.scss';
|
import styles from './styles.scss';
|
||||||
import ErrorContainer from './error/error-container';
|
import ErrorContainer from './error/error-container';
|
||||||
|
|
||||||
import UserContainer from './user/user-container';
|
import AuthenticationContainer from './user/authentication-container';
|
||||||
import ShowUserContainer from './user/show-user-container';
|
import ShowUserContainer from './user/show-user-container';
|
||||||
import ShowApiDetailsContainer from './api/show-api-details-container';
|
import ShowApiDetailsContainer from './api/show-api-details-container';
|
||||||
import { ScrollContainer } from 'react-router-scroll';
|
import { ScrollContainer } from 'react-router-scroll';
|
||||||
@ -136,7 +136,7 @@ export default class App extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<UserContainer />
|
<AuthenticationContainer />
|
||||||
<Layout fixedHeader>
|
<Layout fixedHeader>
|
||||||
<Header title={this.getTitleWithLinks()}>
|
<Header title={this.getTitleWithLinks()}>
|
||||||
<Navigation>
|
<Navigation>
|
||||||
@ -184,6 +184,7 @@ export default class App extends Component {
|
|||||||
{createListItem('/history', 'Event History')}
|
{createListItem('/history', 'Event History')}
|
||||||
{createListItem('/archive', 'Archived Toggles')}
|
{createListItem('/archive', 'Archived Toggles')}
|
||||||
{createListItem('/applications', 'Applications')}
|
{createListItem('/applications', 'Applications')}
|
||||||
|
<a href="/api/admin/user/logout">Sign out</a>
|
||||||
</FooterLinkList>
|
</FooterLinkList>
|
||||||
</FooterDropDownSection>
|
</FooterDropDownSection>
|
||||||
<FooterDropDownSection title="Clients">
|
<FooterDropDownSection title="Clients">
|
||||||
|
@ -12,7 +12,7 @@ const mapStateToProps = createMapper({
|
|||||||
try {
|
try {
|
||||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// nothing
|
// hide error
|
||||||
}
|
}
|
||||||
return { name };
|
return { name };
|
||||||
},
|
},
|
||||||
|
@ -25,6 +25,7 @@ export default class FeatureListComponent extends React.PureComponent {
|
|||||||
this.props.fetchFeatureToggles();
|
this.props.fetchFeatureToggles();
|
||||||
this.props.fetchFeatureMetrics();
|
this.props.fetchFeatureMetrics();
|
||||||
this.timer = setInterval(() => {
|
this.timer = setInterval(() => {
|
||||||
|
// this.props.fetchFeatureToggles();
|
||||||
this.props.fetchFeatureMetrics();
|
this.props.fetchFeatureMetrics();
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
@ -53,7 +53,7 @@ export default connect(
|
|||||||
try {
|
try {
|
||||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// nothing
|
// hide error
|
||||||
}
|
}
|
||||||
return { name };
|
return { name };
|
||||||
},
|
},
|
||||||
|
77
frontend/src/component/user/authentication-component.jsx
Normal file
77
frontend/src/component/user/authentication-component.jsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { Card, CardTitle, CardText } from 'react-mdl';
|
||||||
|
import Modal from 'react-modal';
|
||||||
|
import AuthenticationSimpleComponent from './authentication-simple-component';
|
||||||
|
import AuthenticationCustomComponent from './authentication-custom-component';
|
||||||
|
|
||||||
|
const SIMPLE_TYPE = 'unsecure';
|
||||||
|
|
||||||
|
const customStyles = {
|
||||||
|
overlay: {
|
||||||
|
position: 'fixed',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
zIndex: 99999,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
top: '50%',
|
||||||
|
left: '50%',
|
||||||
|
right: 'auto',
|
||||||
|
bottom: 'auto',
|
||||||
|
marginRight: '-50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
padding: 0,
|
||||||
|
overflow: 'none',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
class AuthComponent extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
user: PropTypes.object.isRequired,
|
||||||
|
unsecureLogin: PropTypes.func.isRequired,
|
||||||
|
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const authDetails = this.props.user.authDetails;
|
||||||
|
if (!authDetails) return null;
|
||||||
|
|
||||||
|
let content;
|
||||||
|
if (authDetails.type === SIMPLE_TYPE) {
|
||||||
|
content = (
|
||||||
|
<AuthenticationSimpleComponent
|
||||||
|
unsecureLogin={this.props.unsecureLogin}
|
||||||
|
authDetails={authDetails}
|
||||||
|
fetchFeatureToggles={this.props.fetchFeatureToggles}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
content = <AuthenticationCustomComponent authDetails={authDetails} />;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Modal isOpen={this.props.user.showDialog} contentLabel="test" style={customStyles}>
|
||||||
|
<Card shadow={0}>
|
||||||
|
<CardTitle
|
||||||
|
expand
|
||||||
|
style={{
|
||||||
|
color: '#fff',
|
||||||
|
background: '#000',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Action required
|
||||||
|
</CardTitle>
|
||||||
|
<CardText>{content}</CardText>
|
||||||
|
</Card>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthComponent;
|
15
frontend/src/component/user/authentication-container.jsx
Normal file
15
frontend/src/component/user/authentication-container.jsx
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { connect } from 'react-redux';
|
||||||
|
import AuthenticationComponent from './authentication-component';
|
||||||
|
import { unsecureLogin } from '../../store/user/actions';
|
||||||
|
import { fetchFeatureToggles } from '../../store/feature-actions';
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
unsecureLogin,
|
||||||
|
fetchFeatureToggles,
|
||||||
|
};
|
||||||
|
|
||||||
|
const mapStateToProps = state => ({
|
||||||
|
user: state.user.toJS(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default connect(mapStateToProps, mapDispatchToProps)(AuthenticationComponent);
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { CardActions, Button } from 'react-mdl';
|
||||||
|
|
||||||
|
class AuthenticationCustomComponent extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
authDetails: PropTypes.object.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const authDetails = this.props.authDetails;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>{authDetails.message}</p>
|
||||||
|
<CardActions style={{ textAlign: 'center' }}>
|
||||||
|
<a href={authDetails.path}>
|
||||||
|
<Button raised colored>
|
||||||
|
Sign In
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</CardActions>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AuthenticationCustomComponent;
|
@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { hashHistory } from 'react-router';
|
||||||
|
import { CardActions, Button, Textfield } from 'react-mdl';
|
||||||
|
|
||||||
|
class SimpleAuthenticationComponent extends React.Component {
|
||||||
|
static propTypes = {
|
||||||
|
authDetails: PropTypes.object.isRequired,
|
||||||
|
unsecureLogin: PropTypes.func.isRequired,
|
||||||
|
fetchFeatureToggles: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = evt => {
|
||||||
|
evt.preventDefault();
|
||||||
|
const email = this.refs.email.inputRef.value;
|
||||||
|
const user = { email };
|
||||||
|
const path = evt.target.action;
|
||||||
|
|
||||||
|
this.props
|
||||||
|
.unsecureLogin(path, user)
|
||||||
|
.then(this.props.fetchFeatureToggles)
|
||||||
|
.then(() => {
|
||||||
|
hashHistory.push('/features');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const authDetails = this.props.authDetails;
|
||||||
|
return (
|
||||||
|
<form onSubmit={this.handleSubmit} action={authDetails.path}>
|
||||||
|
<p>{authDetails.message}</p>
|
||||||
|
<p>
|
||||||
|
This instance of Unleash is not set up with a secure authentication provider. You can read more
|
||||||
|
about{' '}
|
||||||
|
<a href="https://github.com/Unleash/unleash/blob/master/docs/securing-unleash.md" target="_blank">
|
||||||
|
securing Unleash on GitHub
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<Textfield label="Email" name="email" required type="email" ref="email" />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<CardActions style={{ textAlign: 'center' }}>
|
||||||
|
<Button raised colored>
|
||||||
|
Sign in
|
||||||
|
</Button>
|
||||||
|
</CardActions>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SimpleAuthenticationComponent;
|
@ -1,25 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Icon, Tooltip } from 'react-mdl';
|
import styles from './user.scss';
|
||||||
|
|
||||||
export default class ShowUserComponent extends React.Component {
|
export default class ShowUserComponent extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
user: PropTypes.object.isRequired,
|
profile: PropTypes.object,
|
||||||
openEdit: PropTypes.func.isRequired,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
openEdit = evt => {
|
componentDidMount() {
|
||||||
evt.preventDefault();
|
this.props.fetchUser();
|
||||||
this.props.openEdit();
|
}
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const email = this.props.profile ? this.props.profile.email : '';
|
||||||
|
const imageUrl = email ? this.props.profile.imageUrl : '';
|
||||||
return (
|
return (
|
||||||
<a className="mdl-navigation__link" href="#edit-user" onClick={this.openEdit}>
|
<div className={styles.showUser}>
|
||||||
<Tooltip label={this.props.user.userName || 'Unknown'} large>
|
<img src={imageUrl} title={email} alt={email} />
|
||||||
<Icon name="account_circle" />
|
</div>
|
||||||
</Tooltip>
|
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import ShowUserComponent from './show-user-component';
|
import ShowUserComponent from './show-user-component';
|
||||||
import { openEdit } from '../../store/user/actions';
|
import { fetchUser } from '../../store/user/actions';
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
openEdit,
|
fetchUser,
|
||||||
};
|
};
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
const mapStateToProps = state => ({
|
||||||
user: state.user.toJS(),
|
profile: state.user.get('profile'),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent);
|
export default connect(mapStateToProps, mapDispatchToProps)(ShowUserComponent);
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { Textfield, Button } from 'react-mdl';
|
|
||||||
import Modal from 'react-modal';
|
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
overlay: {
|
|
||||||
position: 'fixed',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
backgroundColor: 'rgba(0, 0, 0, 0.75)',
|
|
||||||
zIndex: 99999,
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
top: '50%',
|
|
||||||
left: '50%',
|
|
||||||
right: 'auto',
|
|
||||||
bottom: 'auto',
|
|
||||||
marginRight: '-50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
backgroundColor: '#FFFFFF',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
class EditUserComponent extends React.Component {
|
|
||||||
static propTypes = {
|
|
||||||
user: PropTypes.object.isRequired,
|
|
||||||
updateUserName: PropTypes.func.isRequired,
|
|
||||||
save: PropTypes.func.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
handleSubmit = evt => {
|
|
||||||
evt.preventDefault();
|
|
||||||
this.props.save();
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Modal isOpen={this.props.user.showDialog} contentLabel="test" style={customStyles}>
|
|
||||||
<h2>Action required</h2>
|
|
||||||
<div>
|
|
||||||
<p>You have to specify a username to use Unleash. This will allow us to track your changes.</p>
|
|
||||||
<form onSubmit={this.handleSubmit}>
|
|
||||||
<Textfield
|
|
||||||
label="Username"
|
|
||||||
name="username"
|
|
||||||
required
|
|
||||||
value={this.props.user.userName}
|
|
||||||
onChange={e => this.props.updateUserName(e.target.value)}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Button raised accent>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default EditUserComponent;
|
|
@ -1,14 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import UserComponent from './user-component';
|
|
||||||
import { updateUserName, save } from '../../store/user/actions';
|
|
||||||
|
|
||||||
const mapDispatchToProps = {
|
|
||||||
updateUserName,
|
|
||||||
save,
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapStateToProps = state => ({
|
|
||||||
user: state.user.toJS(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export default connect(mapStateToProps, mapDispatchToProps)(UserComponent);
|
|
5
frontend/src/component/user/user.scss
Normal file
5
frontend/src/component/user/user.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.showUser img {
|
||||||
|
border-radius: 25px;
|
||||||
|
height: 32px;
|
||||||
|
border: 2px solid #ffffff;
|
||||||
|
}
|
@ -7,9 +7,29 @@ function extractLegacyMsg(body) {
|
|||||||
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
return body && body.length > 0 ? body[0].msg : defaultErrorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ServiceError extends Error {
|
||||||
|
constructor() {
|
||||||
|
super(defaultErrorMessage);
|
||||||
|
this.name = 'ServiceError';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AuthenticationError extends Error {
|
||||||
|
constructor(statusCode, body) {
|
||||||
|
super('Authentication required');
|
||||||
|
this.name = 'AuthenticationError';
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.body = body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function throwIfNotSuccess(response) {
|
export function throwIfNotSuccess(response) {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
if (response.status > 399 && response.status < 404) {
|
if (response.status === 401) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
response.json().then(body => reject(new AuthenticationError(response.status, body)));
|
||||||
|
});
|
||||||
|
} else if (response.status > 399 && response.status < 404) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
response.json().then(body => {
|
response.json().then(body => {
|
||||||
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
||||||
@ -19,7 +39,7 @@ export function throwIfNotSuccess(response) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return Promise.reject(new Error(defaultErrorMessage));
|
return Promise.reject(new ServiceError());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Promise.resolve(response);
|
return Promise.resolve(response);
|
||||||
|
20
frontend/src/data/user-api.js
Normal file
20
frontend/src/data/user-api.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { throwIfNotSuccess, headers } from './helper';
|
||||||
|
|
||||||
|
const URI = 'api/admin/user';
|
||||||
|
|
||||||
|
function fetchUser() {
|
||||||
|
return fetch(URI, { credentials: 'include' })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
function unsecureLogin(path, user) {
|
||||||
|
return fetch(path, { method: 'POST', credentials: 'include', headers, body: JSON.stringify(user) })
|
||||||
|
.then(throwIfNotSuccess)
|
||||||
|
.then(response => response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
fetchUser,
|
||||||
|
unsecureLogin,
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import api from '../../data/applications-api';
|
import api from '../../data/applications-api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
|
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
|
||||||
export const ERROR_RECEIVE_ALL_APPLICATIONS = 'ERROR_RECEIVE_ALL_APPLICATIONS';
|
export const ERROR_RECEIVE_ALL_APPLICATIONS = 'ERROR_RECEIVE_ALL_APPLICATIONS';
|
||||||
@ -16,24 +17,19 @@ const recieveApplication = json => ({
|
|||||||
value: json,
|
value: json,
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorReceiveApplications = (statusCode, type = ERROR_RECEIVE_ALL_APPLICATIONS) => ({
|
|
||||||
type,
|
|
||||||
statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function fetchAll() {
|
export function fetchAll() {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
api
|
api
|
||||||
.fetchAll()
|
.fetchAll()
|
||||||
.then(json => dispatch(recieveAllApplications(json)))
|
.then(json => dispatch(recieveAllApplications(json)))
|
||||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function storeApplicationMetaData(appName, key, value) {
|
export function storeApplicationMetaData(appName, key, value) {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
api
|
api
|
||||||
.storeApplicationMetaData(appName, key, value)
|
.storeApplicationMetaData(appName, key, value)
|
||||||
.catch(error => dispatch(errorReceiveApplications(error, ERROR_UPDATING_APPLICATION_DATA)));
|
.catch(dispatchAndThrow(dispatch, ERROR_UPDATING_APPLICATION_DATA));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchApplication(appName) {
|
export function fetchApplication(appName) {
|
||||||
@ -41,5 +37,5 @@ export function fetchApplication(appName) {
|
|||||||
api
|
api
|
||||||
.fetchApplication(appName)
|
.fetchApplication(appName)
|
||||||
.then(json => dispatch(recieveApplication(json)))
|
.then(json => dispatch(recieveApplication(json)))
|
||||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import api from '../data/archive-api';
|
import api from '../data/archive-api';
|
||||||
|
import { dispatchAndThrow } from './util';
|
||||||
|
|
||||||
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
|
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
|
||||||
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
|
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
|
||||||
@ -14,17 +15,12 @@ const reviveToggle = archiveFeatureToggle => ({
|
|||||||
value: archiveFeatureToggle,
|
value: archiveFeatureToggle,
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorReceiveArchive = statusCode => ({
|
|
||||||
type: ERROR_RECEIVE_ARCHIVE,
|
|
||||||
statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function revive(featureToggle) {
|
export function revive(featureToggle) {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
api
|
api
|
||||||
.revive(featureToggle)
|
.revive(featureToggle)
|
||||||
.then(() => dispatch(reviveToggle(featureToggle)))
|
.then(() => dispatch(reviveToggle(featureToggle)))
|
||||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchArchive() {
|
export function fetchArchive() {
|
||||||
@ -32,5 +28,5 @@ export function fetchArchive() {
|
|||||||
api
|
api
|
||||||
.fetchAll()
|
.fetchAll()
|
||||||
.then(json => dispatch(receiveArchive(json)))
|
.then(json => dispatch(receiveArchive(json)))
|
||||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE));
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,8 @@ import {
|
|||||||
|
|
||||||
import { ERROR_UPDATING_STRATEGY, ERROR_CREATING_STRATEGY, ERROR_RECEIVE_STRATEGIES } from './strategy/actions';
|
import { ERROR_UPDATING_STRATEGY, ERROR_CREATING_STRATEGY, ERROR_RECEIVE_STRATEGIES } from './strategy/actions';
|
||||||
|
|
||||||
|
import { FORBIDDEN } from './util';
|
||||||
|
|
||||||
const debug = require('debug')('unleash:error-store');
|
const debug = require('debug')('unleash:error-store');
|
||||||
|
|
||||||
function getInitState() {
|
function getInitState() {
|
||||||
@ -35,6 +37,8 @@ const strategies = (state = getInitState(), action) => {
|
|||||||
case ERROR_CREATING_STRATEGY:
|
case ERROR_CREATING_STRATEGY:
|
||||||
case ERROR_RECEIVE_STRATEGIES:
|
case ERROR_RECEIVE_STRATEGIES:
|
||||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||||
|
case FORBIDDEN:
|
||||||
|
return addErrorIfNotAlreadyInList(state, '403 Forbidden');
|
||||||
case MUTE_ERROR:
|
case MUTE_ERROR:
|
||||||
return state.update('list', list => list.remove(list.indexOf(action.error)));
|
return state.update('list', list => list.remove(list.indexOf(action.error)));
|
||||||
default:
|
default:
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import api from '../data/feature-api';
|
import api from '../data/feature-api';
|
||||||
const debug = require('debug')('unleash:feature-actions');
|
const debug = require('debug')('unleash:feature-actions');
|
||||||
|
import { dispatchAndThrow } from './util';
|
||||||
|
|
||||||
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
|
export const ADD_FEATURE_TOGGLE = 'ADD_FEATURE_TOGGLE';
|
||||||
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
|
export const REMOVE_FEATURE_TOGGLE = 'REMOVE_FEATURE_TOGGLE';
|
||||||
@ -38,13 +39,6 @@ function receiveFeatureToggles(json) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function dispatchAndThrow(dispatch, type) {
|
|
||||||
return error => {
|
|
||||||
dispatch({ type, error, receivedAt: Date.now() });
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchFeatureToggles() {
|
export function fetchFeatureToggles() {
|
||||||
debug('Start fetching feature toggles');
|
debug('Start fetching feature toggles');
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
|
@ -27,7 +27,7 @@ function receiveSeenApps(json) {
|
|||||||
function dispatchAndThrow(dispatch, type) {
|
function dispatchAndThrow(dispatch, type) {
|
||||||
return error => {
|
return error => {
|
||||||
dispatch({ type, error, receivedAt: Date.now() });
|
dispatch({ type, error, receivedAt: Date.now() });
|
||||||
throw error;
|
// throw error;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import api from '../data/history-api';
|
import api from '../data/history-api';
|
||||||
|
import { dispatchAndThrow } from './util';
|
||||||
|
|
||||||
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
|
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
|
||||||
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
||||||
@ -15,17 +16,12 @@ const receiveHistoryforToggle = json => ({
|
|||||||
value: json,
|
value: json,
|
||||||
});
|
});
|
||||||
|
|
||||||
const errorReceiveHistory = statusCode => ({
|
|
||||||
type: ERROR_RECEIVE_HISTORY,
|
|
||||||
statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
export function fetchHistory() {
|
export function fetchHistory() {
|
||||||
return dispatch =>
|
return dispatch =>
|
||||||
api
|
api
|
||||||
.fetchAll()
|
.fetchAll()
|
||||||
.then(json => dispatch(receiveHistory(json)))
|
.then(json => dispatch(receiveHistory(json)))
|
||||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchHistoryForToggle(toggleName) {
|
export function fetchHistoryForToggle(toggleName) {
|
||||||
@ -33,5 +29,5 @@ export function fetchHistoryForToggle(toggleName) {
|
|||||||
api
|
api
|
||||||
.fetchHistoryForToggle(toggleName)
|
.fetchHistoryForToggle(toggleName)
|
||||||
.then(json => dispatch(receiveHistoryforToggle(json)))
|
.then(json => dispatch(receiveHistoryforToggle(json)))
|
||||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY));
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import api from '../../data/strategy-api';
|
import api from '../../data/strategy-api';
|
||||||
import applicationApi from '../../data/applications-api';
|
import applicationApi from '../../data/applications-api';
|
||||||
|
import { dispatchAndThrow } from '../util';
|
||||||
|
|
||||||
export const ADD_STRATEGY = 'ADD_STRATEGY';
|
export const ADD_STRATEGY = 'ADD_STRATEGY';
|
||||||
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
|
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
|
||||||
@ -26,20 +27,8 @@ const receiveStrategies = json => ({
|
|||||||
|
|
||||||
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
||||||
|
|
||||||
const errorReceiveStrategies = statusCode => ({
|
|
||||||
type: ERROR_RECEIVE_STRATEGIES,
|
|
||||||
statusCode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
||||||
|
|
||||||
function dispatchAndThrow(dispatch, type) {
|
|
||||||
return error => {
|
|
||||||
dispatch({ type, error, receivedAt: Date.now() });
|
|
||||||
throw error;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function fetchStrategies() {
|
export function fetchStrategies() {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch(startRequest());
|
dispatch(startRequest());
|
||||||
@ -47,7 +36,7 @@ export function fetchStrategies() {
|
|||||||
return api
|
return api
|
||||||
.fetchAll()
|
.fetchAll()
|
||||||
.then(json => dispatch(receiveStrategies(json)))
|
.then(json => dispatch(receiveStrategies(json)))
|
||||||
.catch(error => dispatch(errorReceiveStrategies(error)));
|
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_STRATEGIES));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,16 +1,38 @@
|
|||||||
export const USER_UPDATE_USERNAME = 'USER_UPDATE_USERNAME';
|
import api from '../../data/user-api';
|
||||||
export const USER_SAVE = 'USER_SAVE';
|
import { dispatchAndThrow } from '../util';
|
||||||
export const USER_EDIT = 'USER_EDIT';
|
export const UPDATE_USER = 'UPDATE_USER';
|
||||||
|
export const START_FETCH_USER = 'START_FETCH_USER';
|
||||||
|
export const ERROR_FETCH_USER = 'ERROR_FETCH_USER';
|
||||||
|
const debug = require('debug')('unleash:user-actions');
|
||||||
|
|
||||||
export const updateUserName = value => ({
|
const updateUser = value => ({
|
||||||
type: USER_UPDATE_USERNAME,
|
type: UPDATE_USER,
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const save = () => ({
|
function handleError(error) {
|
||||||
type: USER_SAVE,
|
debug(error);
|
||||||
});
|
}
|
||||||
|
|
||||||
export const openEdit = () => ({
|
export function fetchUser() {
|
||||||
type: USER_EDIT,
|
debug('Start fetching user');
|
||||||
});
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_USER });
|
||||||
|
|
||||||
|
return api
|
||||||
|
.fetchUser()
|
||||||
|
.then(json => dispatch(updateUser(json)))
|
||||||
|
.catch(dispatchAndThrow(dispatch, ERROR_FETCH_USER));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function unsecureLogin(path, user) {
|
||||||
|
return dispatch => {
|
||||||
|
dispatch({ type: START_FETCH_USER });
|
||||||
|
|
||||||
|
return api
|
||||||
|
.unsecureLogin(path, user)
|
||||||
|
.then(json => dispatch(updateUser(json)))
|
||||||
|
.catch(handleError);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,59 +1,21 @@
|
|||||||
import { Map as $Map } from 'immutable';
|
import { Map as $Map } from 'immutable';
|
||||||
import { USER_UPDATE_USERNAME, USER_SAVE, USER_EDIT } from './actions';
|
import { UPDATE_USER } from './actions';
|
||||||
|
import { AUTH_REQUIRED } from '../util';
|
||||||
|
|
||||||
const COOKIE_NAME = 'username';
|
const userStore = (state = new $Map(), action) => {
|
||||||
|
|
||||||
// Ref: http://stackoverflow.com/questions/10730362/get-cookie-by-name
|
|
||||||
function readCookie() {
|
|
||||||
const nameEQ = `${COOKIE_NAME}=`;
|
|
||||||
const ca = document.cookie.split(';');
|
|
||||||
for (let i = 0; i < ca.length; i++) {
|
|
||||||
let c = ca[i];
|
|
||||||
// eslint-disable-next-line eqeqeq
|
|
||||||
while (c.charAt(0) == ' ') {
|
|
||||||
c = c.substring(1, c.length);
|
|
||||||
}
|
|
||||||
if (c.indexOf(nameEQ) === 0) {
|
|
||||||
return c.substring(nameEQ.length, c.length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeCookie(userName) {
|
|
||||||
document.cookie = `${COOKIE_NAME}=${encodeURIComponent(userName)}; expires=Thu, 18 Dec 2099 12:00:00 UTC`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInitState() {
|
|
||||||
const userName = decodeURIComponent(readCookie(COOKIE_NAME));
|
|
||||||
const showDialog = !userName;
|
|
||||||
return new $Map({ userName, showDialog });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateUserName(state, action) {
|
|
||||||
return state.set('userName', action.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
function save(state) {
|
|
||||||
const userName = state.get('userName');
|
|
||||||
if (userName) {
|
|
||||||
writeCookie(userName);
|
|
||||||
return state.set('showDialog', false);
|
|
||||||
} else {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const settingStore = (state = getInitState(), action) => {
|
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case USER_UPDATE_USERNAME:
|
case UPDATE_USER:
|
||||||
return updateUserName(state, action);
|
state = state
|
||||||
case USER_SAVE:
|
.set('profile', action.value)
|
||||||
return save(state);
|
.set('showDialog', false)
|
||||||
case USER_EDIT:
|
.set('authDetails', undefined);
|
||||||
return state.set('showDialog', true);
|
return state;
|
||||||
|
case AUTH_REQUIRED:
|
||||||
|
state = state.set('authDetails', action.error.body).set('showDialog', true);
|
||||||
|
return state;
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default settingStore;
|
export default userStore;
|
||||||
|
14
frontend/src/store/util.js
Normal file
14
frontend/src/store/util.js
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
export const AUTH_REQUIRED = 'AUTH_REQUIRED';
|
||||||
|
export const FORBIDDEN = 'FORBIDDEN';
|
||||||
|
|
||||||
|
export function dispatchAndThrow(dispatch, type) {
|
||||||
|
return error => {
|
||||||
|
if (error.statusCode === 401) {
|
||||||
|
dispatch({ type: AUTH_REQUIRED, error, receivedAt: Date.now() });
|
||||||
|
} else if (error.statusCode === 403) {
|
||||||
|
dispatch({ type: FORBIDDEN, error, receivedAt: Date.now() });
|
||||||
|
} else {
|
||||||
|
dispatch({ type, error, receivedAt: Date.now() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user