Migrate GlobalStore to mobx
This commit is contained in:
parent
3a29ee9305
commit
892eb618d0
|
|
@ -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');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,45 +1,27 @@
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import {snack} from './GlobalAction';
|
import {currentUser} from '../stores/CurrentUser';
|
||||||
import {tryAuthenticate} from './UserAction';
|
import SnackManager from '../stores/SnackManager';
|
||||||
|
|
||||||
const tokenKey = 'gotify-login-key';
|
axios.interceptors.request.use((config) => {
|
||||||
|
config.headers['X-Gotify-Key'] = currentUser.token();
|
||||||
/**
|
return config;
|
||||||
* 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.response.use(undefined, (error) => {
|
axios.interceptors.response.use(undefined, (error) => {
|
||||||
if (!error.response) {
|
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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const status = error.response.status;
|
const status = error.response.status;
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
tryAuthenticate().then(() => snack('Could not complete request.'));
|
currentUser.tryAuthenticate().then(() => SnackManager.snack('Could not complete request.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (status === 400) {
|
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 Promise.reject(error);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @return {string} the application token
|
|
||||||
*/
|
|
||||||
export function getToken(): string | null {
|
|
||||||
return localStorage.getItem(tokenKey);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,8 @@ import Highlight from '@material-ui/icons/Highlight';
|
||||||
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
|
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
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) => ({
|
const styles = (theme: Theme) => ({
|
||||||
appBar: {
|
appBar: {
|
||||||
|
|
@ -43,6 +44,7 @@ interface IProps {
|
||||||
showSettings: VoidFunction;
|
showSettings: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
class Header extends Component<IProps & Styles> {
|
class Header extends Component<IProps & Styles> {
|
||||||
public render() {
|
public render() {
|
||||||
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
|
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
|
||||||
|
|
@ -107,7 +109,7 @@ class Header extends Component<IProps & Styles> {
|
||||||
|
|
||||||
{name}
|
{name}
|
||||||
</Button>
|
</Button>
|
||||||
<Button color="inherit" onClick={UserAction.logout} id="logout">
|
<Button color="inherit" onClick={currentUser.logout} id="logout">
|
||||||
<ExitToApp />
|
<ExitToApp />
|
||||||
Logout
|
Logout
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,25 @@ import DialogContent from '@material-ui/core/DialogContent';
|
||||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
import Tooltip from '@material-ui/core/Tooltip';
|
import Tooltip from '@material-ui/core/Tooltip';
|
||||||
import React, {ChangeEvent, Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import * as UserAction from '../actions/UserAction';
|
import {currentUser} from '../stores/CurrentUser';
|
||||||
|
import {observable} from 'mobx';
|
||||||
interface IState {
|
import {observer} from 'mobx-react';
|
||||||
pass: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SettingsDialog extends Component<IProps, IState> {
|
@observer
|
||||||
public state = {pass: ''};
|
export default class SettingsDialog extends Component<IProps> {
|
||||||
|
@observable
|
||||||
|
private pass = '';
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {pass} = this.state;
|
const {pass} = this;
|
||||||
const {fClose} = this.props;
|
const {fClose} = this.props;
|
||||||
const submitAndClose = () => {
|
const submitAndClose = () => {
|
||||||
UserAction.changeCurrentUser(pass);
|
currentUser.changePassword(pass);
|
||||||
fClose();
|
fClose();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
|
@ -41,7 +41,7 @@ export default class SettingsDialog extends Component<IProps, IState> {
|
||||||
type="password"
|
type="password"
|
||||||
label="New Pass *"
|
label="New Pass *"
|
||||||
value={pass}
|
value={pass}
|
||||||
onChange={this.handleChange.bind(this, 'pass')}
|
onChange={(e) => (this.pass = e.target.value)}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
@ -63,10 +63,4 @@ export default class SettingsDialog extends Component<IProps, IState> {
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChange(propertyName: string, event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.value;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,22 @@
|
||||||
import Button from '@material-ui/core/Button';
|
import Button from '@material-ui/core/Button';
|
||||||
import Grid from '@material-ui/core/Grid';
|
import Grid from '@material-ui/core/Grid';
|
||||||
import TextField from '@material-ui/core/TextField';
|
import TextField from '@material-ui/core/TextField';
|
||||||
import React, {ChangeEvent, Component, FormEvent} from 'react';
|
import React, {Component, FormEvent} from 'react';
|
||||||
import * as UserAction from '../actions/UserAction';
|
|
||||||
import Container from '../component/Container';
|
import Container from '../component/Container';
|
||||||
import DefaultPage from '../component/DefaultPage';
|
import DefaultPage from '../component/DefaultPage';
|
||||||
|
import {currentUser} from '../stores/CurrentUser';
|
||||||
|
import {observable} from 'mobx';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
|
||||||
interface IState {
|
@observer
|
||||||
username: string;
|
class Login extends Component {
|
||||||
password: string;
|
@observable
|
||||||
}
|
private username = '';
|
||||||
|
@observable
|
||||||
class Login extends Component<{}, IState> {
|
private password = '';
|
||||||
public state = {username: '', password: ''};
|
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {username, password} = this.state;
|
const {username, password} = this;
|
||||||
return (
|
return (
|
||||||
<DefaultPage title="Login" maxWidth={250} hideButton={true}>
|
<DefaultPage title="Login" maxWidth={250} hideButton={true}>
|
||||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||||
|
|
@ -27,7 +28,7 @@ class Login extends Component<{}, IState> {
|
||||||
label="Username"
|
label="Username"
|
||||||
margin="dense"
|
margin="dense"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={this.handleChange.bind(this, 'username')}
|
onChange={(e) => (this.username = e.target.value)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
type="password"
|
type="password"
|
||||||
|
|
@ -35,7 +36,7 @@ class Login extends Component<{}, IState> {
|
||||||
label="Password"
|
label="Password"
|
||||||
margin="normal"
|
margin="normal"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={this.handleChange.bind(this, 'password')}
|
onChange={(e) => (this.password = e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
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>) => {
|
private login = (e: React.MouseEvent<HTMLInputElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
UserAction.login(this.state.username, this.state.password);
|
currentUser.login(this.username, this.password);
|
||||||
};
|
};
|
||||||
|
|
||||||
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
|
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
|
||||||
Loading…
Reference in New Issue