Migrate GlobalStore to mobx

This commit is contained in:
Jannis Mattheis 2018-10-21 13:36:54 +02:00
parent 3a29ee9305
commit 892eb618d0
7 changed files with 144 additions and 259 deletions

View File

@ -1,146 +0,0 @@
import axios, {AxiosResponse} from 'axios';
import {detect} from 'detect-browser';
import * as config from '../config';
import ClientStore from '../stores/ClientStore';
import dispatcher from '../stores/dispatcher';
import {getToken, setAuthorizationToken} from './defaultAxios';
import * as GlobalAction from './GlobalAction';
/**
* Login the user.
* @param {string} username
* @param {string} password
*/
export function login(username: string, password: string) {
const browser = detect();
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
authenticating();
axios
.create()
.request({
url: config.get('url') + 'client',
method: 'POST',
data: {name},
auth: {username, password},
})
.then((resp) => {
GlobalAction.snack(`A client named '${name}' was created for your session.`);
setAuthorizationToken(resp.data.token);
tryAuthenticate()
.then(GlobalAction.initialLoad)
.catch(() =>
console.log(
'create client succeeded, but authenticated with given token failed'
)
);
})
.catch(() => {
GlobalAction.snack('Login failed');
noAuthentication();
});
}
/** Log the user out. */
export function logout() {
const token = getToken();
if (token !== null) {
axios.delete(config.get('url') + 'client/' + ClientStore.getIdByToken(token)).then(() => {
setAuthorizationToken(null);
noAuthentication();
});
}
}
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);
GlobalAction.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
*/
export function changeCurrentUser(pass: string) {
axios
.post(config.get('url') + 'current/user/password', {pass})
.then(() => GlobalAction.snack('Password changed'));
}
/** Fetches all users. */
export function fetchUsers() {
axios.get(config.get('url') + 'user').then((resp: AxiosResponse<IUser[]>) => {
dispatcher.dispatch({type: 'UPDATE_USERS', payload: resp.data});
});
}
/**
* Delete a user.
* @param {int} id the user id
*/
export function deleteUser(id: number) {
axios
.delete(config.get('url') + 'user/' + id)
.then(fetchUsers)
.then(() => GlobalAction.snack('User deleted'));
}
/**
* Create a user.
* @param {string} name
* @param {string} pass
* @param {bool} admin if true, the user is an administrator
*/
export function createUser(name: string, pass: string, admin: boolean) {
axios
.post(config.get('url') + 'user', {name, pass, admin})
.then(fetchUsers)
.then(() => GlobalAction.snack('User created'));
}
/**
* Update a user by id.
* @param {int} id
* @param {string} name
* @param {string} pass empty if no change
* @param {bool} admin if true, the user is an administrator
*/
export function updateUser(id: number, name: string, pass: string | null, admin: boolean) {
axios.post(config.get('url') + 'user/' + id, {name, pass, admin}).then(() => {
fetchUsers();
tryAuthenticate(); // try authenticate updates the current user
GlobalAction.snack('User updated');
});
}

View File

@ -1,45 +1,27 @@
import axios from 'axios';
import {snack} from './GlobalAction';
import {tryAuthenticate} from './UserAction';
import {currentUser} from '../stores/CurrentUser';
import SnackManager from '../stores/SnackManager';
const tokenKey = 'gotify-login-key';
/**
* Set the authorization token for the next requests.
* @param {string|null} token the gotify application token
*/
export function setAuthorizationToken(token: string | null) {
if (token) {
localStorage.setItem(tokenKey, token);
axios.defaults.headers.common['X-Gotify-Key'] = token;
} else {
localStorage.removeItem(tokenKey);
delete axios.defaults.headers.common['X-Gotify-Key'];
}
}
axios.interceptors.request.use((config) => {
config.headers['X-Gotify-Key'] = currentUser.token();
return config;
});
axios.interceptors.response.use(undefined, (error) => {
if (!error.response) {
snack('Gotify server is not reachable, try refreshing the page.');
SnackManager.snack('Gotify server is not reachable, try refreshing the page.');
return Promise.reject(error);
}
const status = error.response.status;
if (status === 401) {
tryAuthenticate().then(() => snack('Could not complete request.'));
currentUser.tryAuthenticate().then(() => SnackManager.snack('Could not complete request.'));
}
if (status === 400) {
snack(error.response.data.error + ': ' + error.response.data.errorDescription);
SnackManager.snack(error.response.data.error + ': ' + error.response.data.errorDescription);
}
return Promise.reject(error);
});
/**
* @return {string} the application token
*/
export function getToken(): string | null {
return localStorage.getItem(tokenKey);
}

View File

@ -12,7 +12,8 @@ import Highlight from '@material-ui/icons/Highlight';
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import * as UserAction from '../actions/UserAction';
import {currentUser} from '../stores/CurrentUser';
import {observer} from 'mobx-react';
const styles = (theme: Theme) => ({
appBar: {
@ -43,6 +44,7 @@ interface IProps {
showSettings: VoidFunction;
}
@observer
class Header extends Component<IProps & Styles> {
public render() {
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
@ -107,7 +109,7 @@ class Header extends Component<IProps & Styles> {
&nbsp;
{name}
</Button>
<Button color="inherit" onClick={UserAction.logout} id="logout">
<Button color="inherit" onClick={currentUser.logout} id="logout">
<ExitToApp />
&nbsp;Logout
</Button>

View File

@ -5,25 +5,25 @@ import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import React, {ChangeEvent, Component} from 'react';
import * as UserAction from '../actions/UserAction';
interface IState {
pass: string;
}
import React, {Component} from 'react';
import {currentUser} from '../stores/CurrentUser';
import {observable} from 'mobx';
import {observer} from 'mobx-react';
interface IProps {
fClose: VoidFunction;
}
export default class SettingsDialog extends Component<IProps, IState> {
public state = {pass: ''};
@observer
export default class SettingsDialog extends Component<IProps> {
@observable
private pass = '';
public render() {
const {pass} = this.state;
const {pass} = this;
const {fClose} = this.props;
const submitAndClose = () => {
UserAction.changeCurrentUser(pass);
currentUser.changePassword(pass);
fClose();
};
return (
@ -41,7 +41,7 @@ export default class SettingsDialog extends Component<IProps, IState> {
type="password"
label="New Pass *"
value={pass}
onChange={this.handleChange.bind(this, 'pass')}
onChange={(e) => (this.pass = e.target.value)}
fullWidth
/>
</DialogContent>
@ -63,10 +63,4 @@ export default class SettingsDialog extends Component<IProps, IState> {
</Dialog>
);
}
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
const state = this.state;
state[propertyName] = event.target.value;
this.setState(state);
}
}

View File

@ -1,21 +1,22 @@
import Button from '@material-ui/core/Button';
import Grid from '@material-ui/core/Grid';
import TextField from '@material-ui/core/TextField';
import React, {ChangeEvent, Component, FormEvent} from 'react';
import * as UserAction from '../actions/UserAction';
import React, {Component, FormEvent} from 'react';
import Container from '../component/Container';
import DefaultPage from '../component/DefaultPage';
import {currentUser} from '../stores/CurrentUser';
import {observable} from 'mobx';
import {observer} from 'mobx-react';
interface IState {
username: string;
password: string;
}
class Login extends Component<{}, IState> {
public state = {username: '', password: ''};
@observer
class Login extends Component {
@observable
private username = '';
@observable
private password = '';
public render() {
const {username, password} = this.state;
const {username, password} = this;
return (
<DefaultPage title="Login" maxWidth={250} hideButton={true}>
<Grid item xs={12} style={{textAlign: 'center'}}>
@ -27,7 +28,7 @@ class Login extends Component<{}, IState> {
label="Username"
margin="dense"
value={username}
onChange={this.handleChange.bind(this, 'username')}
onChange={(e) => (this.username = e.target.value)}
/>
<TextField
type="password"
@ -35,7 +36,7 @@ class Login extends Component<{}, IState> {
label="Password"
margin="normal"
value={password}
onChange={this.handleChange.bind(this, 'password')}
onChange={(e) => (this.password = e.target.value)}
/>
<Button
type="submit"
@ -54,15 +55,9 @@ class Login extends Component<{}, IState> {
);
}
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
const state = this.state;
state[propertyName] = event.target.value;
this.setState(state);
}
private login = (e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault();
UserAction.login(this.state.username, this.state.password);
currentUser.login(this.username, this.password);
};
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();

View File

@ -0,0 +1,106 @@
import axios, {AxiosResponse} from 'axios';
import * as config from '../config';
import {detect} from 'detect-browser';
import * as GlobalAction from '../actions/GlobalAction';
import SnackManager, {SnackReporter} from './SnackManager';
import {observable} from 'mobx';
const tokenKey = 'gotify-login-key';
class CurrentUser {
private tokenCache: string | null = null;
@observable
public loggedIn = false;
@observable
public authenticating = false;
@observable
public user: IUser = {name: 'unknown', admin: false, id: -1};
public constructor(private readonly snack: SnackReporter) {}
public token = (): string => {
if (this.tokenCache !== null) {
return this.tokenCache;
}
const localStorageToken = window.localStorage.getItem(tokenKey);
if (localStorageToken) {
this.tokenCache = localStorageToken;
return localStorageToken;
}
return '';
};
private setToken = (token: string) => {
this.tokenCache = token;
window.localStorage.setItem(tokenKey, token);
};
public login = async (username: string, password: string) => {
this.authenticating = true;
const browser = detect();
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
axios
.create()
.request({
url: config.get('url') + 'client',
method: 'POST',
data: {name},
auth: {username, password},
})
.then((resp: AxiosResponse<IClient>) => {
this.snack(`A client named '${name}' was created for your session.`);
this.setToken(resp.data.token);
this.tryAuthenticate()
.then((user) => {
this.authenticating = false;
this.loggedIn = true;
GlobalAction.initialLoad(user);
})
.catch(() => {
this.authenticating = false;
console.log(
'create client succeeded, but authenticated with given token failed'
);
});
})
.catch(() => {
this.authenticating = false;
return this.snack('Login failed');
});
};
public tryAuthenticate = async (): Promise<AxiosResponse<IUser>> => {
if (this.token() === '') {
return Promise.reject();
}
return axios
.create()
.get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': this.token()}})
.then((passThrough) => {
this.user = passThrough.data;
this.loggedIn = true;
return passThrough;
})
.catch((error) => {
this.logout();
return Promise.reject(error);
});
};
public logout = () => {
window.localStorage.removeItem(tokenKey);
this.tokenCache = null;
this.loggedIn = false;
};
public changePassword = (pass: string) => {
axios
.post(config.get('url') + 'current/user/password', {pass})
.then(() => this.snack('Password changed'));
};
}
export const currentUser = new CurrentUser(SnackManager.snack);

View File

@ -1,48 +0,0 @@
import {EventEmitter} from 'events';
import dispatcher, {IEvent} from './dispatcher';
class GlobalStore extends EventEmitter {
private currentUser: IUser | null = null;
private isAuthenticating = true;
public authenticating(): boolean {
return this.isAuthenticating;
}
public get(): IUser {
return this.currentUser || {name: 'unknown', admin: false, id: -1};
}
public isAdmin(): boolean {
return this.get().admin;
}
public getName(): string {
return this.get().name;
}
public isLoggedIn(): boolean {
return this.currentUser != null;
}
public handle(data: IEvent): void {
if (data.type === 'NO_AUTHENTICATION') {
this.set(null);
} else if (data.type === 'AUTHENTICATED') {
this.set(data.payload);
} else if (data.type === 'AUTHENTICATING') {
this.isAuthenticating = true;
this.emit('change');
}
}
private set(user: IUser | null): void {
this.isAuthenticating = false;
this.currentUser = user;
this.emit('change');
}
}
const store = new GlobalStore();
dispatcher.register(store.handle.bind(store));
export default store;