Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Various stuff #43

Merged
merged 5 commits into from
Apr 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions ui/src/Layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@ import Login from './pages/Login';
import axios from 'axios';
import {createMuiTheme, MuiThemeProvider, withStyles} from 'material-ui/styles';
import config from 'react-global-configuration';
import CurrentUserStore from './stores/CurrentUserStore';
import GlobalStore from './stores/GlobalStore';
import {HashRouter, Redirect, Route, Switch} from 'react-router-dom';
import {getToken} from './actions/defaultAxios';
import Applications from './pages/Applications';
import Clients from './pages/Clients';
import Users from './pages/Users';
import PropTypes from 'prop-types';
import SettingsDialog from './component/SettingsDialog';
import SnackBarHandler from './component/SnackBarHandler';
import LoadingSpinner from './component/LoadingSpinner';

const lightTheme = createMuiTheme({
palette: {
Expand Down Expand Up @@ -49,9 +49,10 @@ class Layout extends Component {
darkTheme: true,
redirect: false,
showSettings: false,
loggedIn: CurrentUserStore.isLoggedIn(),
admin: CurrentUserStore.isAdmin(),
name: CurrentUserStore.getName(),
loggedIn: GlobalStore.isLoggedIn(),
admin: GlobalStore.isAdmin(),
name: GlobalStore.getName(),
authenticating: GlobalStore.authenticating(),
version: Layout.defaultVersion,
};

Expand All @@ -64,47 +65,47 @@ class Layout extends Component {
}

componentWillMount() {
CurrentUserStore.on('change', this.updateUser);
GlobalStore.on('change', this.updateUser);
}

componentWillUnmount() {
CurrentUserStore.removeListener('change', this.updateUser);
GlobalStore.removeListener('change', this.updateUser);
}

toggleTheme = () => this.setState({...this.state, darkTheme: !this.state.darkTheme});

updateUser = () => {
this.setState({
...this.state,
loggedIn: CurrentUserStore.isLoggedIn(),
admin: CurrentUserStore.isAdmin(),
name: CurrentUserStore.getName(),
loggedIn: GlobalStore.isLoggedIn(),
admin: GlobalStore.isAdmin(),
name: GlobalStore.getName(),
authenticating: GlobalStore.authenticating(),
});
};

hideSettings = () => this.setState({...this.state, showSettings: false});
showSettings = () => this.setState({...this.state, showSettings: true});

render() {
const {name, admin, version, loggedIn, showSettings} = this.state;
const {name, admin, version, loggedIn, showSettings, authenticating} = this.state;
const {classes} = this.props;
const theme = this.state.darkTheme ? darkTheme : lightTheme;
return (
<MuiThemeProvider theme={theme}>
<HashRouter>

<div style={{display: 'flex'}}>

<Reboot/>
<Header admin={admin} name={name} version={version} loggedIn={loggedIn}
toggleTheme={this.toggleTheme} showSettings={this.showSettings}/>
<Navigation loggedIn={loggedIn}/>

<main className={classes.content}>
<Switch>
{authenticating ? <Route path="/"><LoadingSpinner/></Route> : null}
<Route exact path="/login" render={() =>
(loggedIn ? (<Redirect to="/"/>) : (<Login/>))}/>
{(loggedIn || getToken() != null) ? null : <Redirect to="/login"/>}
{loggedIn ? null : <Redirect to="/login"/>}
<Route exact path="/" component={Messages}/>
<Route exact path="/messages/:id" component={Messages}/>
<Route exact path="/applications" component={Applications}/>
Expand All @@ -117,7 +118,6 @@ class Layout extends Component {
<SnackBarHandler/>
</div>
</HashRouter>

</MuiThemeProvider>
);
}
Expand Down
8 changes: 4 additions & 4 deletions ui/src/actions/GlobalAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import * as MessageAction from './MessageAction';
import * as ClientAction from './ClientAction';
import dispatcher from '../stores/dispatcher';

/** Calls all actions to initialize the state. */
export function initialLoad() {
export function initialLoad(resp) {
AppAction.fetchApps();
UserAction.fetchCurrentUser();
MessageAction.fetchMessages();
MessageAction.listenToWebSocket();
ClientAction.fetchClients();
UserAction.fetchUsers();
if (resp.data.admin) {
UserAction.fetchUsers();
}
}

export function snack(message) {
Expand Down
20 changes: 15 additions & 5 deletions ui/src/actions/MessageAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import config from 'react-global-configuration';
import axios from 'axios';
import {getToken} from './defaultAxios';
import {snack} from './GlobalAction';
import * as UserAction from './UserAction';

/** Fetches all messages from the current user. */
export function fetchMessages() {
Expand Down Expand Up @@ -33,23 +34,32 @@ export function deleteMessage(id) {
axios.delete(config.get('url') + 'message/' + id).then(fetchMessages).then(() => snack('Message deleted'));
}

let wsActive = false;

/**
* Starts listening to the stream for new messages.
*/
export function listenToWebSocket() {
if (!getToken()) {
if (!getToken() || wsActive) {
return;
}
wsActive = true;

const wsUrl = config.get('url').replace('http', 'ws').replace('https', 'wss');
const ws = new WebSocket(wsUrl + 'stream?token=' + getToken());

ws.onerror = (e) => {
console.log('WebSocket connection errored; trying again in 60 seconds', e);
snack('Could not connect to the web socket, trying again in 60 seconds.');
setTimeout(listenToWebSocket, 60000);
wsActive = false;
console.log('WebSocket connection errored', e);
};

ws.onmessage = (data) => dispatcher.dispatch({type: 'ONE_MESSAGE', payload: JSON.parse(data.data)});

ws.onclose = (data) => console.log('WebSocket closed, this normally means the client was deleted.', data);
ws.onclose = () => {
wsActive = false;
UserAction.tryAuthenticate().then(() => {
snack('WebSocket connection closed, trying again in 30 seconds.');
setTimeout(listenToWebSocket, 30000);
});
};
}
46 changes: 38 additions & 8 deletions ui/src/actions/UserAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,34 +15,64 @@ import {snack} from './GlobalAction';
export function login(username, password) {
const browser = detect();
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
authenticating();
axios.create().request(config.get('url') + 'client', {
method: 'POST',
data: {name: name},
auth: {username: username, password: password},
}).then(function(resp) {
snack(`A client named '${name}' was created for your session.`);
setAuthorizationToken(resp.data.token);
GlobalAction.initialLoad();
}).catch(() => snack('Login failed'));
tryAuthenticate().then(GlobalAction.initialLoad)
.catch(() => console.log('create client succeeded, but authenticated with given token failed'));
}).catch(() => {
snack('Login failed');
noAuthentication();
});
}

/** Log the user out. */
export function logout() {
if (getToken() !== null) {
axios.delete(config.get('url') + 'client/' + ClientStore.getIdByToken(getToken())).then(() => {
setAuthorizationToken(null);
dispatcher.dispatch({type: 'REMOVE_CURRENT_USER'});
noAuthentication();
});
}
}

/** Fetches the current user. */
export function fetchCurrentUser() {
axios.get(config.get('url') + 'current/user').then(function(resp) {
dispatcher.dispatch({type: 'SET_CURRENT_USER', payload: resp.data});
export function tryAuthenticate() {
return axios.create().get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': getToken()}}).then((resp) => {
dispatcher.dispatch({type: 'AUTHENTICATED', payload: resp.data});
return resp;
}).catch((resp) => {
if (getToken()) {
setAuthorizationToken(null);
snack('Authentication failed, try to re-login. (client or user was deleted)');
}
noAuthentication();
return Promise.reject(resp);
});
}

export function checkIfAlreadyLoggedIn() {
const token = getToken();
if (token) {
setAuthorizationToken(token);
tryAuthenticate().then(GlobalAction.initialLoad);
} else {
noAuthentication();
}
}

function noAuthentication() {
dispatcher.dispatch({type: 'NO_AUTHENTICATION'});
}

function authenticating() {
dispatcher.dispatch({type: 'AUTHENTICATING'});
}

/**
* Changes the current user.
* @param {string} pass
Expand Down Expand Up @@ -86,7 +116,7 @@ export function createUser(name, pass, admin) {
export function updateUser(id, name, pass, admin) {
axios.post(config.get('url') + 'user/' + id, {name, pass, admin}).then(function() {
fetchUsers();
fetchCurrentUser(); // just in case update current user
tryAuthenticate(); // try authenticate updates the current user
snack('User updated');
});
}
30 changes: 11 additions & 19 deletions ui/src/actions/defaultAxios.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
import axios from 'axios';
import dispatcher from '../stores/dispatcher';
import * as GlobalAction from './GlobalAction';
import {snack} from './GlobalAction';
import {tryAuthenticate} from './UserAction';

let currentToken = null;
const tokenKey = 'gotify-login-key';

/**
* Set the authorization token for the next requests.
* @param {string} token the gotify application token
* @param {string|null} token the gotify application token
*/
export function setAuthorizationToken(token) {
currentToken = token;
if (token) {
localStorage.setItem(tokenKey, token);
axios.defaults.headers.common['X-Gotify-Key'] = token;
Expand All @@ -27,10 +24,14 @@ axios.interceptors.response.use(undefined, (error) => {
return Promise.reject(error);
}

if (error.response.status === 401) {
snack('Authentication failed');
setAuthorizationToken(null);
dispatcher.dispatch({type: 'REMOVE_CURRENT_USER'});
const status = error.response.status;

if (status === 401) {
tryAuthenticate().then(() => snack('Could not complete request.'));
}

if (status === 400) {
snack(error.response.data.error + ': ' + error.response.data.errorDescription);
}

return Promise.reject(error);
Expand All @@ -40,14 +41,5 @@ axios.interceptors.response.use(undefined, (error) => {
* @return {string} the application token
*/
export function getToken() {
return currentToken;
}

/** Checks if the current user is logged, if so update the state. */
export function checkIfAlreadyLoggedIn() {
const key = localStorage.getItem(tokenKey);
if (key) {
setAuthorizationToken(key);
GlobalAction.initialLoad();
}
return localStorage.getItem(tokenKey);
}
18 changes: 18 additions & 0 deletions ui/src/component/LoadingSpinner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, {Component} from 'react';
import {CircularProgress} from 'material-ui/Progress';
import DefaultPage from './DefaultPage';
import Grid from 'material-ui/Grid';

class LoadingSpinner extends Component {
render() {
return (
<DefaultPage title="" maxWidth={250} hideButton={true}>
<Grid item xs={12} style={{textAlign: 'center'}}>
<CircularProgress size={150}/>
</Grid>
</DefaultPage>
);
}
}

export default LoadingSpinner;
2 changes: 1 addition & 1 deletion ui/src/component/SettingsDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default class SettingsDialog extends Component {
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip title={pass.length === 0 ? '' : 'pass is required'}>
<Tooltip title={pass.length !== 0 ? '' : 'pass is required'}>
<div>
<Button disabled={pass.length === 0} onClick={submitAndClose} color="primary"
variant="raised">
Expand Down
15 changes: 10 additions & 5 deletions ui/src/component/SnackBarHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SnackBarHandler extends Component {

state = {
current: '',
hasNext: false,
open: false,
openWhen: 0,
};
Expand Down Expand Up @@ -40,26 +41,30 @@ class SnackBarHandler extends Component {
open: true,
openWhen: Date.now(),
current: SnackBarStore.next(),
hasNext: SnackBarStore.hasNext(),
});
}
};

closeCurrentSnack = () => this.setState({...this.state, open: false});

render() {
const {open, current} = this.state;
const {open, current, hasNext} = this.state;
const duration = hasNext
? SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS
: SnackBarHandler.MAX_VISIBLE_SNACK_TIME_IN_MS;

return (
<Snackbar
anchorOrigin={{vertical: 'bottom', horizontal: 'left'}}
open={open} autoHideDuration={SnackBarHandler.MAX_VISIBLE_SNACK_TIME_IN_MS}
open={open} autoHideDuration={duration}
onClose={this.closeCurrentSnack} onExited={this.openNextSnack}
message={<span id="message-id">{current}</span>}
action={[
action={
<IconButton key="close" aria-label="Close" color="inherit" onClick={this.closeCurrentSnack}>
<Close/>
</IconButton>,
]}
</IconButton>
}
/>
);
}
Expand Down
4 changes: 2 additions & 2 deletions ui/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Layout from './Layout';
import registerServiceWorker from './registerServiceWorker';
import {checkIfAlreadyLoggedIn} from './actions/defaultAxios';
import config from 'react-global-configuration';
import * as Notifications from './stores/Notifications';
import 'typeface-roboto';
import 'typeface-roboto-mono';
import * as UserAction from './actions/UserAction';

const defaultDevConfig = {
url: 'http://localhost:80/',
Expand All @@ -29,7 +29,7 @@ const defaultProdConfig = {
} else {
config.set(window.config || defaultDevConfig);
}
checkIfAlreadyLoggedIn();
UserAction.checkIfAlreadyLoggedIn();
ReactDOM.render(<Layout/>, document.getElementById('root'));
registerServiceWorker();
}());
Loading