mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-27 00:19:39 +01: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 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 ShowApiDetailsContainer from './api/show-api-details-container';
|
||||
import { ScrollContainer } from 'react-router-scroll';
|
||||
@ -136,7 +136,7 @@ export default class App extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<UserContainer />
|
||||
<AuthenticationContainer />
|
||||
<Layout fixedHeader>
|
||||
<Header title={this.getTitleWithLinks()}>
|
||||
<Navigation>
|
||||
@ -184,6 +184,7 @@ export default class App extends Component {
|
||||
{createListItem('/history', 'Event History')}
|
||||
{createListItem('/archive', 'Archived Toggles')}
|
||||
{createListItem('/applications', 'Applications')}
|
||||
<a href="/api/admin/user/logout">Sign out</a>
|
||||
</FooterLinkList>
|
||||
</FooterDropDownSection>
|
||||
<FooterDropDownSection title="Clients">
|
||||
|
@ -12,7 +12,7 @@ const mapStateToProps = createMapper({
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||
} catch (e) {
|
||||
// nothing
|
||||
// hide error
|
||||
}
|
||||
return { name };
|
||||
},
|
||||
|
@ -25,6 +25,7 @@ export default class FeatureListComponent extends React.PureComponent {
|
||||
this.props.fetchFeatureToggles();
|
||||
this.props.fetchFeatureMetrics();
|
||||
this.timer = setInterval(() => {
|
||||
// this.props.fetchFeatureToggles();
|
||||
this.props.fetchFeatureMetrics();
|
||||
}, 5000);
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ export default connect(
|
||||
try {
|
||||
[, name] = document.location.hash.match(/name=([a-z0-9-_.]+)/i);
|
||||
} catch (e) {
|
||||
// nothing
|
||||
// hide error
|
||||
}
|
||||
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 PropTypes from 'prop-types';
|
||||
import { Icon, Tooltip } from 'react-mdl';
|
||||
import styles from './user.scss';
|
||||
|
||||
export default class ShowUserComponent extends React.Component {
|
||||
static propTypes = {
|
||||
user: PropTypes.object.isRequired,
|
||||
openEdit: PropTypes.func.isRequired,
|
||||
profile: PropTypes.object,
|
||||
};
|
||||
|
||||
openEdit = evt => {
|
||||
evt.preventDefault();
|
||||
this.props.openEdit();
|
||||
};
|
||||
componentDidMount() {
|
||||
this.props.fetchUser();
|
||||
}
|
||||
|
||||
render() {
|
||||
const email = this.props.profile ? this.props.profile.email : '';
|
||||
const imageUrl = email ? this.props.profile.imageUrl : '';
|
||||
return (
|
||||
<a className="mdl-navigation__link" href="#edit-user" onClick={this.openEdit}>
|
||||
<Tooltip label={this.props.user.userName || 'Unknown'} large>
|
||||
<Icon name="account_circle" />
|
||||
</Tooltip>
|
||||
</a>
|
||||
<div className={styles.showUser}>
|
||||
<img src={imageUrl} title={email} alt={email} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import { connect } from 'react-redux';
|
||||
import ShowUserComponent from './show-user-component';
|
||||
import { openEdit } from '../../store/user/actions';
|
||||
import { fetchUser } from '../../store/user/actions';
|
||||
|
||||
const mapDispatchToProps = {
|
||||
openEdit,
|
||||
fetchUser,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.user.toJS(),
|
||||
profile: state.user.get('profile'),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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) => {
|
||||
response.json().then(body => {
|
||||
const errorMsg = body && body.isJoi ? extractJoiMsg(body) : extractLegacyMsg(body);
|
||||
@ -19,7 +39,7 @@ export function throwIfNotSuccess(response) {
|
||||
});
|
||||
});
|
||||
} else {
|
||||
return Promise.reject(new Error(defaultErrorMessage));
|
||||
return Promise.reject(new ServiceError());
|
||||
}
|
||||
}
|
||||
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 { dispatchAndThrow } from '../util';
|
||||
|
||||
export const RECEIVE_ALL_APPLICATIONS = 'RECEIVE_ALL_APPLICATIONS';
|
||||
export const ERROR_RECEIVE_ALL_APPLICATIONS = 'ERROR_RECEIVE_ALL_APPLICATIONS';
|
||||
@ -16,24 +17,19 @@ const recieveApplication = json => ({
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveApplications = (statusCode, type = ERROR_RECEIVE_ALL_APPLICATIONS) => ({
|
||||
type,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchAll() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(recieveAllApplications(json)))
|
||||
.catch(error => dispatch(errorReceiveApplications(error)));
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ALL_APPLICATIONS));
|
||||
}
|
||||
|
||||
export function storeApplicationMetaData(appName, key, value) {
|
||||
return dispatch =>
|
||||
api
|
||||
.storeApplicationMetaData(appName, key, value)
|
||||
.catch(error => dispatch(errorReceiveApplications(error, ERROR_UPDATING_APPLICATION_DATA)));
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_UPDATING_APPLICATION_DATA));
|
||||
}
|
||||
|
||||
export function fetchApplication(appName) {
|
||||
@ -41,5 +37,5 @@ export function fetchApplication(appName) {
|
||||
api
|
||||
.fetchApplication(appName)
|
||||
.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 { dispatchAndThrow } from './util';
|
||||
|
||||
export const REVIVE_TOGGLE = 'REVIVE_TOGGLE';
|
||||
export const RECEIVE_ARCHIVE = 'RECEIVE_ARCHIVE';
|
||||
@ -14,17 +15,12 @@ const reviveToggle = archiveFeatureToggle => ({
|
||||
value: archiveFeatureToggle,
|
||||
});
|
||||
|
||||
const errorReceiveArchive = statusCode => ({
|
||||
type: ERROR_RECEIVE_ARCHIVE,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function revive(featureToggle) {
|
||||
return dispatch =>
|
||||
api
|
||||
.revive(featureToggle)
|
||||
.then(() => dispatch(reviveToggle(featureToggle)))
|
||||
.catch(error => dispatch(errorReceiveArchive(error)));
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_ARCHIVE));
|
||||
}
|
||||
|
||||
export function fetchArchive() {
|
||||
@ -32,5 +28,5 @@ export function fetchArchive() {
|
||||
api
|
||||
.fetchAll()
|
||||
.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 { FORBIDDEN } from './util';
|
||||
|
||||
const debug = require('debug')('unleash:error-store');
|
||||
|
||||
function getInitState() {
|
||||
@ -35,6 +37,8 @@ const strategies = (state = getInitState(), action) => {
|
||||
case ERROR_CREATING_STRATEGY:
|
||||
case ERROR_RECEIVE_STRATEGIES:
|
||||
return addErrorIfNotAlreadyInList(state, action.error.message);
|
||||
case FORBIDDEN:
|
||||
return addErrorIfNotAlreadyInList(state, '403 Forbidden');
|
||||
case MUTE_ERROR:
|
||||
return state.update('list', list => list.remove(list.indexOf(action.error)));
|
||||
default:
|
||||
|
@ -1,5 +1,6 @@
|
||||
import api from '../data/feature-api';
|
||||
const debug = require('debug')('unleash:feature-actions');
|
||||
import { dispatchAndThrow } from './util';
|
||||
|
||||
export const ADD_FEATURE_TOGGLE = 'ADD_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() {
|
||||
debug('Start fetching feature toggles');
|
||||
return dispatch => {
|
||||
|
@ -27,7 +27,7 @@ function receiveSeenApps(json) {
|
||||
function dispatchAndThrow(dispatch, type) {
|
||||
return error => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
// throw error;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import api from '../data/history-api';
|
||||
import { dispatchAndThrow } from './util';
|
||||
|
||||
export const RECEIVE_HISTORY = 'RECEIVE_HISTORY';
|
||||
export const ERROR_RECEIVE_HISTORY = 'ERROR_RECEIVE_HISTORY';
|
||||
@ -15,17 +16,12 @@ const receiveHistoryforToggle = json => ({
|
||||
value: json,
|
||||
});
|
||||
|
||||
const errorReceiveHistory = statusCode => ({
|
||||
type: ERROR_RECEIVE_HISTORY,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
export function fetchHistory() {
|
||||
return dispatch =>
|
||||
api
|
||||
.fetchAll()
|
||||
.then(json => dispatch(receiveHistory(json)))
|
||||
.catch(error => dispatch(errorReceiveHistory(error)));
|
||||
.catch(dispatchAndThrow(dispatch, ERROR_RECEIVE_HISTORY));
|
||||
}
|
||||
|
||||
export function fetchHistoryForToggle(toggleName) {
|
||||
@ -33,5 +29,5 @@ export function fetchHistoryForToggle(toggleName) {
|
||||
api
|
||||
.fetchHistoryForToggle(toggleName)
|
||||
.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 applicationApi from '../../data/applications-api';
|
||||
import { dispatchAndThrow } from '../util';
|
||||
|
||||
export const ADD_STRATEGY = 'ADD_STRATEGY';
|
||||
export const UPDATE_STRATEGY = 'UPDATE_STRATEGY';
|
||||
@ -26,20 +27,8 @@ const receiveStrategies = json => ({
|
||||
|
||||
const startCreate = () => ({ type: START_CREATE_STRATEGY });
|
||||
|
||||
const errorReceiveStrategies = statusCode => ({
|
||||
type: ERROR_RECEIVE_STRATEGIES,
|
||||
statusCode,
|
||||
});
|
||||
|
||||
const startUpdate = () => ({ type: START_UPDATE_STRATEGY });
|
||||
|
||||
function dispatchAndThrow(dispatch, type) {
|
||||
return error => {
|
||||
dispatch({ type, error, receivedAt: Date.now() });
|
||||
throw error;
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStrategies() {
|
||||
return dispatch => {
|
||||
dispatch(startRequest());
|
||||
@ -47,7 +36,7 @@ export function fetchStrategies() {
|
||||
return api
|
||||
.fetchAll()
|
||||
.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';
|
||||
export const USER_SAVE = 'USER_SAVE';
|
||||
export const USER_EDIT = 'USER_EDIT';
|
||||
import api from '../../data/user-api';
|
||||
import { dispatchAndThrow } from '../util';
|
||||
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 => ({
|
||||
type: USER_UPDATE_USERNAME,
|
||||
const updateUser = value => ({
|
||||
type: UPDATE_USER,
|
||||
value,
|
||||
});
|
||||
|
||||
export const save = () => ({
|
||||
type: USER_SAVE,
|
||||
});
|
||||
function handleError(error) {
|
||||
debug(error);
|
||||
}
|
||||
|
||||
export const openEdit = () => ({
|
||||
type: USER_EDIT,
|
||||
});
|
||||
export function fetchUser() {
|
||||
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 { USER_UPDATE_USERNAME, USER_SAVE, USER_EDIT } from './actions';
|
||||
import { UPDATE_USER } from './actions';
|
||||
import { AUTH_REQUIRED } from '../util';
|
||||
|
||||
const COOKIE_NAME = 'username';
|
||||
|
||||
// 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) => {
|
||||
const userStore = (state = new $Map(), action) => {
|
||||
switch (action.type) {
|
||||
case USER_UPDATE_USERNAME:
|
||||
return updateUserName(state, action);
|
||||
case USER_SAVE:
|
||||
return save(state);
|
||||
case USER_EDIT:
|
||||
return state.set('showDialog', true);
|
||||
case UPDATE_USER:
|
||||
state = state
|
||||
.set('profile', action.value)
|
||||
.set('showDialog', false)
|
||||
.set('authDetails', undefined);
|
||||
return state;
|
||||
case AUTH_REQUIRED:
|
||||
state = state.set('authDetails', action.error.body).set('showDialog', true);
|
||||
return state;
|
||||
default:
|
||||
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