add authorization
This commit is contained in:
@ -7,7 +7,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="description" content="Web site created by cool programmers" />
|
||||
<title>Crypto bot: dev</title>
|
||||
<title>Crypto bot: Admin</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
99
src/app/components/login-layout/LoginLayout.tsx
Normal file
99
src/app/components/login-layout/LoginLayout.tsx
Normal file
@ -0,0 +1,99 @@
|
||||
import {Button, Card, Form, Input, Layout} from 'antd';
|
||||
import {useForm} from 'antd/lib/form/Form';
|
||||
import {AxiosError} from 'axios';
|
||||
import React, {FC, memo, useCallback, useState} from 'react';
|
||||
import {createUseStyles} from 'react-jss';
|
||||
import authServiceApi from '../../../core/infrastructure/AuthServiceAPI';
|
||||
|
||||
type LoginModel = {
|
||||
login: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
input: {
|
||||
marginBottom: '16px'
|
||||
},
|
||||
layout: {
|
||||
height: '100%'
|
||||
},
|
||||
content: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center'
|
||||
},
|
||||
});
|
||||
|
||||
const formItemLayout = {
|
||||
labelCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 8}
|
||||
},
|
||||
wrapperCol: {
|
||||
xs: {span: 24},
|
||||
sm: {span: 16}
|
||||
}
|
||||
};
|
||||
|
||||
const LoginLayout: FC = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
const [disabled, setDisabled] = useState(false);
|
||||
|
||||
const [form] = useForm<LoginModel>();
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const {login, password} = form.getFieldsValue();
|
||||
setDisabled(true);
|
||||
authServiceApi.auth(login, password).catch((error: AxiosError) => {
|
||||
const message = error.response?.data.message;
|
||||
const errors = message ? [message] : ['Some error'];
|
||||
const validationParams = {
|
||||
validating: false,
|
||||
errors
|
||||
};
|
||||
form.setFields([
|
||||
{...validationParams, name: 'login'},
|
||||
{...validationParams, name: 'password'}
|
||||
]);
|
||||
setDisabled(false);
|
||||
});
|
||||
}, [form]);
|
||||
|
||||
return (
|
||||
<Layout className={classes.layout}>
|
||||
<Layout.Content className={classes.content}>
|
||||
<Card title="Admin Panel" style={{width: 300}}>
|
||||
<Form {...formItemLayout} form={form} onFinish={handleSubmit}>
|
||||
<Form.Item
|
||||
hasFeedback
|
||||
className={classes.input}
|
||||
name="login"
|
||||
label="Login:"
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<Input disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
hasFeedback
|
||||
className={classes.input}
|
||||
name="password"
|
||||
label="Password:"
|
||||
rules={[{required: true}]}
|
||||
>
|
||||
<Input.Password disabled={disabled} />
|
||||
</Form.Item>
|
||||
<Form.Item wrapperCol={{offset: 8, span: 16}}>
|
||||
<Button htmlType="submit" disabled={disabled}>
|
||||
Sign in
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(LoginLayout);
|
||||
1
src/app/components/login-layout/index.ts
Normal file
1
src/app/components/login-layout/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export {default} from './LoginLayout';
|
||||
@ -1,20 +1,42 @@
|
||||
import {Layout} from 'antd';
|
||||
import {Button, Layout} from 'antd';
|
||||
import React, {FC, memo, PropsWithChildren} from 'react';
|
||||
import {createUseStyles} from 'react-jss';
|
||||
import authServiceApi from '../../../core/infrastructure/AuthServiceAPI';
|
||||
import Menu from '../menu';
|
||||
|
||||
const useStyles = createUseStyles({
|
||||
layout: {
|
||||
height: '100%',
|
||||
},
|
||||
container: {
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
buttonContainer: {
|
||||
margin: 'auto 16px 16px 16px',
|
||||
},
|
||||
button: {
|
||||
width: '100%',
|
||||
}
|
||||
});
|
||||
|
||||
const handleSignOut = () => {
|
||||
authServiceApi.signOut();
|
||||
};
|
||||
|
||||
const MainLayout: FC<PropsWithChildren<unknown>> = ({children}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<Layout className={classes.layout}>
|
||||
<Layout.Sider>
|
||||
<div className={classes.container}>
|
||||
<Menu />
|
||||
<div className={classes.buttonContainer}>
|
||||
<Button type="primary" className={classes.button} onClick={handleSignOut}>Sign out</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
<Layout>
|
||||
<Layout.Content>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, {memo} from 'react';
|
||||
import React, {FC, Fragment, memo, useEffect} from 'react';
|
||||
import {Route, Switch} from 'react-router-dom';
|
||||
import {context} from '@reatom/react';
|
||||
import {context, useAtom} from '@reatom/react';
|
||||
import mainPageRouter from '_pages/main/routing';
|
||||
import usersPageRouter from '_pages/users/routing';
|
||||
import actionsPageRouter from '_pages/actions/routing';
|
||||
@ -13,6 +13,9 @@ import jss from 'jss';
|
||||
import preset from 'jss-preset-default';
|
||||
import {store} from '../../../core/infrastructure/atom/store';
|
||||
import ConnectedRouter from '../../../core/blocks/connected-router/ConnectedRouter';
|
||||
import {authAtom} from '../../../core/infrastructure/atom/authAtom';
|
||||
import LoginLayout from '../login-layout';
|
||||
import authServiceApi from '../../../core/infrastructure/AuthServiceAPI';
|
||||
|
||||
jss.setup(preset());
|
||||
|
||||
@ -33,10 +36,11 @@ const styles = {
|
||||
|
||||
jss.createStyleSheet(styles).attach();
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const SecurityRoutes: FC = memo(() => {
|
||||
const isAuth = useAtom(authAtom);
|
||||
return (
|
||||
<context.Provider value={store}>
|
||||
<ConnectedRouter>
|
||||
<Fragment>
|
||||
{isAuth && (
|
||||
<MainLayout>
|
||||
<Switch>
|
||||
{mainPageRouter}
|
||||
@ -50,6 +54,23 @@ const Page: React.FC = () => {
|
||||
</Route>
|
||||
</Switch>
|
||||
</MainLayout>
|
||||
)}
|
||||
{!isAuth && (
|
||||
<LoginLayout />
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
});
|
||||
|
||||
const Page: FC = () => {
|
||||
useEffect(() => {
|
||||
authServiceApi.refresh();
|
||||
});
|
||||
|
||||
return (
|
||||
<context.Provider value={store}>
|
||||
<ConnectedRouter>
|
||||
<SecurityRoutes />
|
||||
</ConnectedRouter>
|
||||
</context.Provider>
|
||||
);
|
||||
|
||||
50
src/core/infrastructure/AuthServiceAPI.ts
Normal file
50
src/core/infrastructure/AuthServiceAPI.ts
Normal file
@ -0,0 +1,50 @@
|
||||
import axios from 'axios';
|
||||
import {bindedActions as authActions} from './atom/authAtom';
|
||||
|
||||
import {tokenAPI} from './TokenAPI';
|
||||
|
||||
type AuthResponse = {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
};
|
||||
|
||||
const ENDPOINT = 'https://api.crypto-bot.vigdorov.ru/auth';
|
||||
|
||||
class AuthServiceApi {
|
||||
auth = (login: string, password: string) => {
|
||||
return axios.post<AuthResponse>(`${ENDPOINT}/auth-user`, {
|
||||
login,
|
||||
password,
|
||||
}).then(({data: {access_token, refresh_token}}) => {
|
||||
tokenAPI.setTokens(access_token, refresh_token);
|
||||
authActions.changeAuth(true);
|
||||
});
|
||||
}
|
||||
|
||||
refresh = () => {
|
||||
const refreshToken = tokenAPI.getRefreshToken();
|
||||
if (refreshToken) {
|
||||
return axios.post<AuthResponse>(`${ENDPOINT}/refresh-tokens`, {refresh_token: refreshToken})
|
||||
.then(({data: {access_token, refresh_token}}) => {
|
||||
tokenAPI.setTokens(access_token, refresh_token);
|
||||
authActions.changeAuth(true);
|
||||
})
|
||||
.catch(e => {
|
||||
tokenAPI.clearTokents();
|
||||
authActions.changeAuth(false);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.reject(new Error('Не авторизован'));
|
||||
}
|
||||
|
||||
signOut = () => {
|
||||
tokenAPI.clearTokents();
|
||||
authActions.changeAuth(false);
|
||||
}
|
||||
}
|
||||
|
||||
const authServiceApi = new AuthServiceApi();
|
||||
|
||||
export default authServiceApi;
|
||||
@ -1,4 +1,5 @@
|
||||
import axios, {AxiosRequestConfig} from 'axios';
|
||||
import {AxiosRequestConfig} from 'axios';
|
||||
import httpAuthApi from './HttpAuthAPI';
|
||||
|
||||
enum Method {
|
||||
Get = 'get',
|
||||
@ -16,7 +17,7 @@ type RequestConfig<Q, B> = Omit<AxiosRequestConfig, 'params' | 'data'> & {
|
||||
};
|
||||
|
||||
const requestMiddleware = async <Q, B, R>(config: RequestConfig<Q, B>): Promise<R> => {
|
||||
const axiosResponse = await axios.request<R>(config);
|
||||
const axiosResponse = await httpAuthApi.request<R>(config);
|
||||
// Добавить обработку ошибок
|
||||
return axiosResponse.data;
|
||||
};
|
||||
|
||||
92
src/core/infrastructure/HttpAuthAPI.ts
Normal file
92
src/core/infrastructure/HttpAuthAPI.ts
Normal file
@ -0,0 +1,92 @@
|
||||
import axios, {AxiosError, AxiosRequestConfig, AxiosResponse} from 'axios';
|
||||
import authServiceApi from './AuthServiceAPI';
|
||||
import {tokenAPI} from './TokenAPI';
|
||||
|
||||
type HandlerArgs<T> = {
|
||||
resolve: (v: AxiosResponse<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
requestConfig: AxiosRequestConfig
|
||||
error?: AxiosError<T>,
|
||||
}
|
||||
|
||||
type Handler<T> = (args: HandlerArgs<T>) => void;
|
||||
|
||||
class HttpAuthApi {
|
||||
pendingRequests: HandlerArgs<any>[];
|
||||
|
||||
isPendingRefresh: boolean;
|
||||
|
||||
constructor() {
|
||||
this.pendingRequests = [];
|
||||
this.isPendingRefresh = false;
|
||||
}
|
||||
|
||||
processPendingRequests = (handler: Handler<unknown>) => {
|
||||
this.isPendingRefresh = false;
|
||||
while (this.pendingRequests.length) {
|
||||
const pendingRequest = this.pendingRequests.shift()!;
|
||||
handler(pendingRequest);
|
||||
}
|
||||
}
|
||||
|
||||
repeatRequests = () => {
|
||||
this.processPendingRequests(({resolve, reject, requestConfig}) => {
|
||||
this.request(requestConfig)
|
||||
.then(resolve)
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
rejectRequests = (error: AxiosError) => {
|
||||
this.processPendingRequests(({reject}) => {
|
||||
reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
errorHandling = (
|
||||
error: AxiosError<unknown>,
|
||||
resolve: (v: AxiosResponse<any>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
requestConfig: AxiosRequestConfig
|
||||
) => {
|
||||
if (error.response?.status === 401) {
|
||||
this.isPendingRefresh = true;
|
||||
this.pendingRequests.push({resolve, reject, requestConfig});
|
||||
authServiceApi.refresh()
|
||||
.then(() => {
|
||||
this.repeatRequests();
|
||||
})
|
||||
.catch((refreshError: AxiosError) => {
|
||||
this.rejectRequests(refreshError);
|
||||
tokenAPI.clearTokents();
|
||||
location.reload();
|
||||
});
|
||||
} else {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
addTokenToRequest = (requestConfig: AxiosRequestConfig): AxiosRequestConfig => ({
|
||||
...requestConfig,
|
||||
headers: {
|
||||
...(requestConfig.headers ?? {}),
|
||||
'Authorization': tokenAPI.getAccessToken(),
|
||||
},
|
||||
})
|
||||
|
||||
request = <T>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isPendingRefresh) {
|
||||
this.pendingRequests.push({resolve, reject, requestConfig});
|
||||
} else {
|
||||
axios(this.addTokenToRequest(requestConfig))
|
||||
.then(response => resolve(response))
|
||||
.catch(error => this.errorHandling(error, resolve, reject, requestConfig));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const httpAuthApi = new HttpAuthApi();
|
||||
|
||||
export default httpAuthApi;
|
||||
35
src/core/infrastructure/StorageAPI.ts
Normal file
35
src/core/infrastructure/StorageAPI.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export class StorageAPI<T> {
|
||||
private _init: T;
|
||||
|
||||
private _storage: Storage;
|
||||
|
||||
private _stateName: string;
|
||||
|
||||
constructor(init: T, stateName: string, storageType: 'local' | 'session' = 'local') {
|
||||
this._init = init;
|
||||
this._stateName = stateName;
|
||||
this._storage = storageType === 'local' ? localStorage : sessionStorage;
|
||||
|
||||
if (!this._storage.getItem(stateName)) {
|
||||
this.set(this._init);
|
||||
}
|
||||
}
|
||||
|
||||
set(updatedState: T) {
|
||||
this._storage.setItem(this._stateName, JSON.stringify(updatedState));
|
||||
}
|
||||
|
||||
get(): T {
|
||||
const stringValue = this._storage.getItem(this._stateName) || '';
|
||||
|
||||
try {
|
||||
return JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
return this._init;
|
||||
}
|
||||
}
|
||||
|
||||
clear() {
|
||||
this._storage.removeItem(this._stateName);
|
||||
}
|
||||
}
|
||||
32
src/core/infrastructure/TokenAPI.ts
Normal file
32
src/core/infrastructure/TokenAPI.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import {StorageAPI} from './StorageAPI';
|
||||
|
||||
class TokenAPI {
|
||||
private accessTokenAPI: StorageAPI<string>;
|
||||
|
||||
private refreshTokenAPI: StorageAPI<string>;
|
||||
|
||||
constructor() {
|
||||
this.accessTokenAPI = new StorageAPI('', 'access', 'session');
|
||||
this.refreshTokenAPI = new StorageAPI('', 'refresh', 'local');
|
||||
}
|
||||
|
||||
setTokens(accessToken: string, refreshToken: string) {
|
||||
this.accessTokenAPI.set(accessToken);
|
||||
this.refreshTokenAPI.set(refreshToken);
|
||||
}
|
||||
|
||||
getAccessToken() {
|
||||
return this.accessTokenAPI.get();
|
||||
}
|
||||
|
||||
getRefreshToken() {
|
||||
return this.refreshTokenAPI.get();
|
||||
}
|
||||
|
||||
clearTokents() {
|
||||
this.accessTokenAPI.clear();
|
||||
this.refreshTokenAPI.clear();
|
||||
}
|
||||
}
|
||||
|
||||
export const tokenAPI = new TokenAPI();
|
||||
14
src/core/infrastructure/atom/authAtom.ts
Normal file
14
src/core/infrastructure/atom/authAtom.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import {declareAction, declareAtom} from '@reatom/core';
|
||||
import {store} from './store';
|
||||
|
||||
const authAction = declareAction<boolean>();
|
||||
|
||||
export const authAtom = declareAtom(false, on => [
|
||||
on(authAction, (_state, payload) => payload),
|
||||
]);
|
||||
|
||||
export const bindedActions = {
|
||||
changeAuth: (isAuth: boolean) => {
|
||||
store.dispatch(authAction(isAuth));
|
||||
},
|
||||
};
|
||||
@ -1,7 +0,0 @@
|
||||
import {declareAction, declareAtom} from '@reatom/core';
|
||||
|
||||
export const changeNameAction = declareAction<string>();
|
||||
|
||||
export const nameAtom = declareAtom('', on => [
|
||||
on(changeNameAction, (_state, payload) => payload),
|
||||
]);
|
||||
@ -1,21 +0,0 @@
|
||||
export const makeLocalStorageService = <T>(init: T, stateName: string) => {
|
||||
if (!localStorage.getItem(stateName)) {
|
||||
localStorage.setItem(stateName, JSON.stringify(init));
|
||||
}
|
||||
|
||||
return {
|
||||
set: (updatedState: T) => {
|
||||
localStorage.setItem(stateName, JSON.stringify(updatedState));
|
||||
return updatedState;
|
||||
},
|
||||
get: (): T => {
|
||||
const stringValue = localStorage.getItem(stateName) || '';
|
||||
|
||||
try {
|
||||
return JSON.parse(stringValue);
|
||||
} catch (e) {
|
||||
return init;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -16,7 +16,7 @@ describe('jsonParse', () => {
|
||||
});
|
||||
|
||||
it('Должен вернуть undefined для не корректных значений', () => {
|
||||
expect(jsonParse()).toBeUndefined();
|
||||
expect(jsonParse(undefined)).toBeUndefined();
|
||||
expect(jsonParse('')).toBeUndefined();
|
||||
expect(jsonParse(' ')).toBeUndefined();
|
||||
expect(jsonParse('{"9')).toBeUndefined();
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
export const jsonParse = <T>(str?: string, defaultValue?: T): Undefinable<T> => {
|
||||
export function jsonParse<T>(str: Undefinable<string>, defaultValue: T): T;
|
||||
export function jsonParse<T>(str: Undefinable<string>, defaultValue?: T): Undefinable<T>;
|
||||
export function jsonParse<T>(str: Undefinable<string>, defaultValue?: T) {
|
||||
const trimStr = str?.trim();
|
||||
try {
|
||||
const parsedValue = JSON.parse(trimStr ?? '');
|
||||
@ -7,4 +9,4 @@ export const jsonParse = <T>(str?: string, defaultValue?: T): Undefinable<T> =>
|
||||
} catch (e) {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user