mirror of
https://github.com/sergeyyarkov/educt-client.git
synced 2026-10-11 10:19:22 +03:00
feat: setup auth
This commit is contained in:
parent
05b79a0875
commit
8ef0d4f8f2
34
src/App.tsx
34
src/App.tsx
@ -1,21 +1,43 @@
|
||||
import * as React from 'react';
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import PrivateRoute from './components/PrivateRoute/PrivateRoute';
|
||||
import { ChakraProvider, theme } from '@chakra-ui/react';
|
||||
import { useRootStore } from './hooks/useRootStore';
|
||||
import { observer } from 'mobx-react';
|
||||
|
||||
/**
|
||||
* Pages
|
||||
*/
|
||||
import MainPage from './pages/main';
|
||||
import AuthPage from './pages/auth';
|
||||
import NotFoundPage from './pages/404';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { autorun } from 'mobx';
|
||||
|
||||
export const App = () => (
|
||||
<ChakraProvider theme={theme}>
|
||||
const App = () => {
|
||||
const rootStore = useRootStore();
|
||||
const toast = useToast();
|
||||
|
||||
React.useEffect(() => {
|
||||
autorun(() => {
|
||||
if (rootStore.networkApiError !== undefined) {
|
||||
toast({
|
||||
title: `Network error while connecting to the server.`,
|
||||
description: 'Try your request later.',
|
||||
status: 'error',
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Switch>
|
||||
<PrivateRoute exact path='/' component={MainPage} title='Main' />
|
||||
<Route path='/auth' component={AuthPage} />
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</Router>
|
||||
</ChakraProvider>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(App);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { MdAccountCircle, MdVpnKey } from 'react-icons/md';
|
||||
import {
|
||||
FormControl,
|
||||
@ -12,44 +12,39 @@ import {
|
||||
import { useRootStore } from '../../hooks/useRootStore';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { observer } from 'mobx-react';
|
||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
||||
|
||||
type AuthFormState = {
|
||||
type AuthInputs = {
|
||||
login: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
const AuthForm: React.FC = () => {
|
||||
const [authFormState, setAuthFormState] = useState<AuthFormState>({
|
||||
login: '',
|
||||
password: '',
|
||||
});
|
||||
const { register, reset, handleSubmit } = useForm<AuthInputs>();
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
const handleLoginForm = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
const { login, password } = authFormState;
|
||||
await authStore.login(login, password);
|
||||
const onSubmit: SubmitHandler<AuthInputs> = async (data) => {
|
||||
try {
|
||||
await authStore.login(data.login, data.password);
|
||||
history.push('/');
|
||||
} catch (error) {
|
||||
console.log('error while log in ');
|
||||
} finally {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleLoginForm}>
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<FormControl isRequired={true}>
|
||||
<FormLabel>Login</FormLabel>
|
||||
<InputGroup>
|
||||
<InputLeftElement children={<Icon as={MdAccountCircle} />} />
|
||||
<Input
|
||||
type='text'
|
||||
name='login'
|
||||
placeholder='email@mail.com'
|
||||
value={authFormState.login}
|
||||
onChange={(e) =>
|
||||
setAuthFormState({
|
||||
...authFormState,
|
||||
login: e.target.value,
|
||||
})
|
||||
}
|
||||
{...register('login', { required: true })}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
@ -59,20 +54,14 @@ const AuthForm: React.FC = () => {
|
||||
<InputLeftElement children={<Icon as={MdVpnKey} />} />
|
||||
<Input
|
||||
type='password'
|
||||
name='password'
|
||||
placeholder='********'
|
||||
value={authFormState.password}
|
||||
onChange={(e) =>
|
||||
setAuthFormState({
|
||||
...authFormState,
|
||||
password: e.target.value,
|
||||
})
|
||||
}
|
||||
{...register('password', { required: true })}
|
||||
/>
|
||||
</InputGroup>
|
||||
</FormControl>
|
||||
<Button
|
||||
loadingText='Logging in...'
|
||||
isLoading={authStore.loading}
|
||||
type='submit'
|
||||
colorScheme='blue'
|
||||
variant='outline'
|
||||
@ -81,7 +70,7 @@ const AuthForm: React.FC = () => {
|
||||
Log in
|
||||
</Button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(AuthForm)
|
||||
export default observer(AuthForm);
|
||||
|
||||
21
src/components/Header/AppName.tsx
Normal file
21
src/components/Header/AppName.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box, Text, Heading } from '@chakra-ui/react';
|
||||
import { MdSchool } from 'react-icons/md';
|
||||
|
||||
const AppName: React.FC = () => {
|
||||
return (
|
||||
<Flex alignItems='center'>
|
||||
<Box mr='4'>
|
||||
<MdSchool fill='blue' size='36px' />
|
||||
</Box>
|
||||
<Box lineHeight='1'>
|
||||
<Heading as='p' fontSize='2xl' color='gray.700'>
|
||||
App name
|
||||
</Heading>
|
||||
<Text as='small'>App description</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default AppName;
|
||||
37
src/components/Header/Header.tsx
Normal file
37
src/components/Header/Header.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import AppName from './AppName';
|
||||
import UserInfo from './User/UserInfo';
|
||||
import UserNotifications from './User/UserNotifications';
|
||||
|
||||
const Header: React.FC = () => {
|
||||
return (
|
||||
<Box
|
||||
as='header'
|
||||
background='#fff'
|
||||
position='fixed'
|
||||
top={0}
|
||||
left={0}
|
||||
right={0}
|
||||
borderBottomWidth={1}
|
||||
width='100%'
|
||||
height='4rem'
|
||||
zIndex={4}>
|
||||
<Flex
|
||||
justifyContent='space-between'
|
||||
width='100%'
|
||||
height='100%'
|
||||
alignItems='center'
|
||||
paddingLeft='1.5rem'
|
||||
paddingRight='1.5rem'>
|
||||
<AppName />
|
||||
<Flex>
|
||||
<UserNotifications />
|
||||
<UserInfo />
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
67
src/components/Header/User/UserInfo.tsx
Normal file
67
src/components/Header/User/UserInfo.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Menu,
|
||||
MenuGroup,
|
||||
MenuList,
|
||||
MenuDivider,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
Button,
|
||||
Flex,
|
||||
Box,
|
||||
Avatar,
|
||||
Text,
|
||||
Link,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdSettings, MdExitToApp } from 'react-icons/md';
|
||||
import { observer } from 'mobx-react';
|
||||
import { useRootStore } from '../../../hooks/useRootStore';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
const UserInfo: React.FC = () => {
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
const onLogoutHandler = async (
|
||||
e: React.MouseEvent<HTMLButtonElement, MouseEvent>
|
||||
) => {
|
||||
try {
|
||||
await authStore.logout();
|
||||
history.push('/auth');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flex alignItems='center'>
|
||||
<Menu>
|
||||
<MenuButton as={Button} pr={6}>
|
||||
<Flex alignItems='center'>
|
||||
<Avatar size='sm' name='user name' marginRight={3} />
|
||||
<Text as='span' mr={2}>
|
||||
Username
|
||||
</Text>
|
||||
</Flex>
|
||||
</MenuButton>
|
||||
<MenuList mr='1rem'>
|
||||
<MenuGroup title='User fullname'>
|
||||
<MenuDivider />
|
||||
<Link to='/profile'>
|
||||
<MenuItem>
|
||||
<Box as={MdSettings} mr={2} />
|
||||
My profile
|
||||
</MenuItem>
|
||||
</Link>
|
||||
<MenuItem onClick={onLogoutHandler}>
|
||||
<Box as={MdExitToApp} mr={2} />
|
||||
Logout
|
||||
</MenuItem>
|
||||
</MenuGroup>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(UserInfo);
|
||||
31
src/components/Header/User/UserNotifications.tsx
Normal file
31
src/components/Header/User/UserNotifications.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react';
|
||||
import { Box } from '@chakra-ui/layout';
|
||||
import { Button } from '@chakra-ui/button';
|
||||
import { MdNotifications } from 'react-icons/md';
|
||||
|
||||
const UserNotifications: React.FC = () => {
|
||||
return (
|
||||
<Button
|
||||
marginRight={3}
|
||||
variant='solid'
|
||||
outline='none'
|
||||
border='none'
|
||||
_after={{
|
||||
content: '"2"',
|
||||
display: 'block',
|
||||
position: 'absolute',
|
||||
top: '8px',
|
||||
right: '10px',
|
||||
background: '#E53E3E',
|
||||
color: '#fff',
|
||||
fontSize: '11px',
|
||||
width: '15px',
|
||||
height: '15px',
|
||||
borderRadius: '100%',
|
||||
}}>
|
||||
<Box as={MdNotifications} boxSize='21px' />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserNotifications;
|
||||
@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import Header from '../Header/Header';
|
||||
|
||||
/**
|
||||
*
|
||||
@ -8,6 +9,8 @@ import { Flex, Box } from '@chakra-ui/react';
|
||||
*/
|
||||
const Layout: React.FC = ({ children }) => {
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<Flex minH='100vh' flexDirection='column'>
|
||||
<Box pl='18rem' mt='4rem' mb='4rem' flex='1 0 auto'>
|
||||
<Box
|
||||
@ -23,6 +26,7 @@ const Layout: React.FC = ({ children }) => {
|
||||
</Box>
|
||||
</Box>
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { ColorModeScript } from '@chakra-ui/react';
|
||||
import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react';
|
||||
import * as React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { App } from './App';
|
||||
import App from './App';
|
||||
import { RootStoreProvider } from './providers';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
|
||||
@ -9,7 +9,9 @@ ReactDOM.render(
|
||||
<>
|
||||
<ColorModeScript />
|
||||
<RootStoreProvider>
|
||||
<ChakraProvider theme={theme}>
|
||||
<App />
|
||||
</ChakraProvider>
|
||||
</RootStoreProvider>
|
||||
</>,
|
||||
document.getElementById('root')
|
||||
|
||||
26
src/pages/404.tsx
Normal file
26
src/pages/404.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Box, Heading, Button } from '@chakra-ui/react';
|
||||
|
||||
const NotFoundPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Error 404</title>
|
||||
</Helmet>
|
||||
<Box textAlign='center' mt={40}>
|
||||
<Heading display='block' as='h1' fontSize='6xl'>
|
||||
Error 404
|
||||
</Heading>
|
||||
<Link to='/'>
|
||||
<Button variant='link' colorScheme='blue'>
|
||||
Return to main page
|
||||
</Button>
|
||||
</Link>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFoundPage;
|
||||
@ -1,11 +1,8 @@
|
||||
import { observer } from 'mobx-react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Flex,
|
||||
Box,
|
||||
} from '@chakra-ui/react';
|
||||
import { MdSchool } from "react-icons/md";
|
||||
import Helmet from 'react-helmet';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Flex, Box } from '@chakra-ui/react';
|
||||
import { MdSchool } from 'react-icons/md';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { useRootStore } from '../hooks/useRootStore';
|
||||
import { IPageProps } from '../interfaces';
|
||||
@ -19,19 +16,24 @@ const AuthPage: React.FC<IPageProps> = () => {
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (authStore.isLoggedIn) {
|
||||
history.push('/');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>Auth</title>
|
||||
</Helmet>
|
||||
<Flex minHeight='100vh' align='center' justifyContent='center'>
|
||||
<Flex
|
||||
minHeight='100vh'
|
||||
align='center'
|
||||
justifyContent='center'
|
||||
padding='5'>
|
||||
<Box
|
||||
p={8}
|
||||
my={40}
|
||||
width='full'
|
||||
maxWidth='450px'
|
||||
borderWidth={1}
|
||||
@ -41,7 +43,7 @@ const AuthPage: React.FC<IPageProps> = () => {
|
||||
<MdSchool fill='blue' size='64px' />
|
||||
</Flex>
|
||||
</Box>
|
||||
<Box my='30px'>
|
||||
<Box>
|
||||
<AuthForm />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Heading,
|
||||
@ -9,11 +9,22 @@ import {
|
||||
} from '@chakra-ui/react';
|
||||
|
||||
import { IPageProps } from '../interfaces';
|
||||
import { useRootStore } from '../hooks/useRootStore';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
/**
|
||||
* Main page
|
||||
*/
|
||||
const MainPage: React.FC<IPageProps> = ({ title }) => {
|
||||
const { authStore } = useRootStore();
|
||||
const history = useHistory();
|
||||
|
||||
useEffect(() => {
|
||||
if (!authStore.isLoggedIn) {
|
||||
history.push('/auth');
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Breadcrumb fontWeight='medium' fontSize='sm'>
|
||||
|
||||
@ -2,12 +2,13 @@ import React, { ReactNode } from 'react';
|
||||
import { StoreContext } from '../contexts';
|
||||
import RootStore from '../stores/RootStore';
|
||||
|
||||
let store: RootStore;
|
||||
|
||||
/**
|
||||
* Root store provider
|
||||
*/
|
||||
export const RootStoreProvider = ({ children }: { children: ReactNode }) => {
|
||||
const root = store ?? new RootStore();
|
||||
return <StoreContext.Provider value={root}>{children}</StoreContext.Provider>;
|
||||
const store = new RootStore();
|
||||
|
||||
return (
|
||||
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@ -20,15 +20,29 @@ export default class ApiService {
|
||||
* Response
|
||||
*/
|
||||
this.api.interceptors.response.use(undefined, (error) => {
|
||||
if (error.response) {
|
||||
/**
|
||||
* Unauthorized
|
||||
* Internal Server Error 500
|
||||
*/
|
||||
if (
|
||||
error.response.status === 401 ||
|
||||
error.response.data.message === '401 Unauthorized'
|
||||
) {
|
||||
if (error.response.status === 500) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unauthorized Error 401
|
||||
*/
|
||||
if (error.response.status === 401) {
|
||||
if (this.root.authStore.isLoggedIn) {
|
||||
this.root.authStore.setIsLoggedIn(false);
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear error state from root */
|
||||
if (this.root.networkApiError !== undefined) {
|
||||
this.root.setNetworkApiError(undefined);
|
||||
}
|
||||
} else {
|
||||
this.root.setNetworkApiError(error);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import ApiService from './ApiService';
|
||||
import { IDataResult } from '../interfaces';
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
interface ILoginResult extends IDataResult {
|
||||
export interface ILoginResult extends IDataResult {
|
||||
data: {
|
||||
token: string;
|
||||
type: string;
|
||||
@ -10,21 +10,39 @@ interface ILoginResult extends IDataResult {
|
||||
}
|
||||
|
||||
export default class AuthService {
|
||||
public apiService: ApiService;
|
||||
public api: AxiosInstance;
|
||||
|
||||
constructor(apiService: ApiService) {
|
||||
this.apiService = apiService;
|
||||
constructor(api: AxiosInstance) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new token
|
||||
*
|
||||
* @param login User login
|
||||
* @param password User password
|
||||
* @returns Login result
|
||||
*/
|
||||
public async requestLogin(
|
||||
login: string,
|
||||
password: string
|
||||
): Promise<ILoginResult> {
|
||||
const result = await this.apiService.api.post('v1/auth/login', {
|
||||
const result = await this.api.post('v1/auth/login', {
|
||||
login,
|
||||
password,
|
||||
});
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke token
|
||||
*
|
||||
* @returns Logout result
|
||||
*/
|
||||
public async requestLogout(): Promise<any> {
|
||||
const result = await this.api.post('v1/auth/logout');
|
||||
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
|
||||
15
src/services/UserService.ts
Normal file
15
src/services/UserService.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { AxiosInstance } from 'axios';
|
||||
|
||||
export default class UserService {
|
||||
public api: AxiosInstance;
|
||||
|
||||
constructor(api: AxiosInstance) {
|
||||
this.api = api;
|
||||
}
|
||||
|
||||
public async fetchMe(): Promise<any> {
|
||||
const result = await this.api.get('v1/me');
|
||||
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
@ -9,6 +9,8 @@ export default class AuthStore {
|
||||
|
||||
public isLoggedIn: boolean = !!Cookies.get('logged_in');
|
||||
|
||||
public loading: boolean = false;
|
||||
|
||||
constructor(root: RootStore) {
|
||||
this.root = root;
|
||||
makeAutoObservable(this);
|
||||
@ -19,17 +21,44 @@ export default class AuthStore {
|
||||
this.isLoggedIn = value;
|
||||
}
|
||||
|
||||
public setLoading(value: boolean) {
|
||||
this.loading = value;
|
||||
}
|
||||
|
||||
public setToken(token: string) {
|
||||
Cookies.set('logged_in', 'true');
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
public async login(login: string, password: string) {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
const result = await this.root.authService.requestLogin(login, password);
|
||||
|
||||
this.setIsLoggedIn(true);
|
||||
this.setToken(result.data.token);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
public logout() {}
|
||||
public async logout() {
|
||||
try {
|
||||
this.setLoading(true);
|
||||
const result = await this.root.authService.requestLogout();
|
||||
|
||||
this.setIsLoggedIn(false);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
} finally {
|
||||
this.setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,16 @@ import AuthStore from './AuthStore';
|
||||
|
||||
import ApiService from '../services/ApiService';
|
||||
import AuthService from '../services/AuthService';
|
||||
import UserStore from './UserStore';
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
|
||||
export default class RootStore {
|
||||
public networkApiError: any;
|
||||
|
||||
public authStore: AuthStore;
|
||||
|
||||
public userStore: UserStore;
|
||||
|
||||
public apiService: ApiService;
|
||||
|
||||
public authService: AuthService;
|
||||
@ -16,6 +22,7 @@ export default class RootStore {
|
||||
* Stores
|
||||
*/
|
||||
this.authStore = new AuthStore(this);
|
||||
this.userStore = new UserStore(this);
|
||||
|
||||
/**
|
||||
* Services
|
||||
@ -23,7 +30,13 @@ export default class RootStore {
|
||||
this.apiService = new ApiService(this, {
|
||||
baseURL: API_BASE_URL,
|
||||
responseType: 'json',
|
||||
withCredentials: true,
|
||||
});
|
||||
this.authService = new AuthService(this.apiService);
|
||||
this.authService = new AuthService(this.apiService.api);
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
public setNetworkApiError(error: any) {
|
||||
this.networkApiError = error;
|
||||
}
|
||||
}
|
||||
|
||||
20
src/stores/UserStore.ts
Normal file
20
src/stores/UserStore.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { makeAutoObservable } from 'mobx';
|
||||
import RootStore from './RootStore';
|
||||
|
||||
export interface IUser {
|
||||
id: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export default class UserStore {
|
||||
public root: RootStore;
|
||||
|
||||
public user: IUser | null = null;
|
||||
|
||||
constructor(root: RootStore) {
|
||||
this.root = root;
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user