diff --git a/ui/src/actions/UserAction.ts b/ui/src/actions/UserAction.ts deleted file mode 100644 index eb0e7d6..0000000 --- a/ui/src/actions/UserAction.ts +++ /dev/null @@ -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) => { - 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'); - }); -} diff --git a/ui/src/actions/defaultAxios.ts b/ui/src/actions/defaultAxios.ts index f480192..6e74259 100644 --- a/ui/src/actions/defaultAxios.ts +++ b/ui/src/actions/defaultAxios.ts @@ -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); -} diff --git a/ui/src/component/Header.tsx b/ui/src/component/Header.tsx index 3d8b8d3..6dd1fcf 100644 --- a/ui/src/component/Header.tsx +++ b/ui/src/component/Header.tsx @@ -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 { public render() { const {classes, version, name, loggedIn, admin, toggleTheme} = this.props; @@ -107,7 +109,7 @@ class Header extends Component {   {name} - diff --git a/ui/src/component/SettingsDialog.tsx b/ui/src/component/SettingsDialog.tsx index 05683db..c206d65 100644 --- a/ui/src/component/SettingsDialog.tsx +++ b/ui/src/component/SettingsDialog.tsx @@ -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 { - public state = {pass: ''}; +@observer +export default class SettingsDialog extends Component { + @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 { type="password" label="New Pass *" value={pass} - onChange={this.handleChange.bind(this, 'pass')} + onChange={(e) => (this.pass = e.target.value)} fullWidth /> @@ -63,10 +63,4 @@ export default class SettingsDialog extends Component { ); } - - private handleChange(propertyName: string, event: ChangeEvent) { - const state = this.state; - state[propertyName] = event.target.value; - this.setState(state); - } } diff --git a/ui/src/pages/Login.tsx b/ui/src/pages/Login.tsx index 3fbdf16..b3a0d34 100644 --- a/ui/src/pages/Login.tsx +++ b/ui/src/pages/Login.tsx @@ -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 ( @@ -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)} /> { label="Password" margin="normal" value={password} - onChange={this.handleChange.bind(this, 'password')} + onChange={(e) => (this.password = e.target.value)} />