Refactored store

This commit is contained in:
cracklesparkle
2024-06-25 15:56:00 +09:00
parent 85f97e9e0e
commit 18fb120777
15 changed files with 205 additions and 113 deletions

View File

@ -8,7 +8,7 @@ import MainLayout from "./layouts/MainLayout"
import SignIn from "./pages/auth/SignIn" import SignIn from "./pages/auth/SignIn"
import ApiTest from "./pages/ApiTest" import ApiTest from "./pages/ApiTest"
import SignUp from "./pages/auth/SignUp" import SignUp from "./pages/auth/SignUp"
import { useAuthStore } from "./store/auth" import { initAuth, useAuthStore } from "./store/auth"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import { CircularProgress } from "@mui/material" import { CircularProgress } from "@mui/material"
@ -17,7 +17,7 @@ function App() {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
useEffect(() => { useEffect(() => {
auth.initializeAuth() initAuth()
}, []) }, [])
// Once auth is there, set loading to false and render the app // Once auth is there, set loading to false and render the app

View File

@ -13,7 +13,7 @@ export default function DataTable(props: Props) {
columns={props.columns} columns={props.columns}
initialState={{ initialState={{
pagination: { pagination: {
paginationModel: { page: 0, pageSize: 5 }, paginationModel: { page: 0, pageSize: 10 },
}, },
}} }}
pageSizeOptions={[10, 20, 50, 100]} pageSizeOptions={[10, 20, 50, 100]}

View File

@ -1,13 +1,12 @@
import { memo, useEffect, useMemo, useState } from "react"; import { memo, useEffect, useMemo, useState } from "react";
import UserService from "../services/UserService"; import UserService from "../services/UserService";
import AuthService from "../services/AuthService";
export default function useUserData<T>(token: string, initData: T): T { export default function useUserData<T>(token: string, initData: T): T {
const [userData, setUserData] = useState<T>(initData) const [userData, setUserData] = useState<T>(initData)
useEffect(()=> { useEffect(()=> {
const fetchUserData = async (token: string) => { const fetchUserData = async (token: string) => {
const response = await AuthService.getCurrentUser(token) const response = await UserService.getCurrentUser(token)
setUserData(response.data) setUserData(response.data)
} }

View File

@ -0,0 +1,4 @@
export const USER_DATA_KEY = 'userData';
export const TOKEN_AUTH_KEY = 'authToken'
export const TOKEN_ISSUED_DATE_KEY = 'tokenIssuedDate';
export const TOKEN_EXPIRY_DURATION = 7 * 24 * 60 * 60 * 1000;

View File

@ -18,4 +18,4 @@ axiosInstance.interceptors.request.use(
} }
); );
export default axiosInstance; export default axiosInstance;

View File

@ -1,3 +1,27 @@
export interface User {
id: number;
}
export interface UserData extends User {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
role_id: number;
}
export interface UserCreds extends User {
password: string;
}
export interface AuthState {
isAuthenticated: boolean;
token: string | null;
userData: UserData | {};
}
export interface SignUpFormData { export interface SignUpFormData {
email: string; email: string;
login: string; login: string;

View File

@ -1,6 +1,6 @@
// Layout for dashboard with responsive drawer // Layout for dashboard with responsive drawer
import { Navigate, Outlet } from "react-router-dom" import { Link, NavLink, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"
import * as React from 'react'; import * as React from 'react';
import AppBar from '@mui/material/AppBar'; import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box'; import Box from '@mui/material/Box';
@ -17,7 +17,8 @@ import MenuIcon from '@mui/icons-material/Menu';
import Toolbar from '@mui/material/Toolbar'; import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography'; import Typography from '@mui/material/Typography';
import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material"; import { Api, ExitToApp, Home, People, Settings, Shield } from "@mui/icons-material";
import { UserData, useAuthStore } from "../store/auth"; import { getUserData, useAuthStore } from "../store/auth";
import { UserData } from "../interfaces/auth";
const drawerWidth = 240; const drawerWidth = 240;
@ -25,6 +26,10 @@ export default function DashboardLayout() {
const authStore = useAuthStore(); const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>(); const [userData, setUserData] = React.useState<UserData>();
const location = useLocation()
const navigate = useNavigate()
//const { window } = props; //const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false); const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false); const [isClosing, setIsClosing] = React.useState(false);
@ -95,7 +100,12 @@ export default function DashboardLayout() {
<List> <List>
{pages.map((item, index) => ( {pages.map((item, index) => (
<ListItem key={index} disablePadding> <ListItem key={index} disablePadding>
<ListItemButton href={item.path}> <ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon> <ListItemIcon>
{item.icon} {item.icon}
</ListItemIcon> </ListItemIcon>
@ -110,7 +120,12 @@ export default function DashboardLayout() {
<List> <List>
{misc.map((item, index) => ( {misc.map((item, index) => (
<ListItem key={index} disablePadding> <ListItem key={index} disablePadding>
<ListItemButton href={item.path}> <ListItemButton
onClick={() => {
navigate(item.path)
}}
selected={location.pathname === item.path}
>
<ListItemIcon> <ListItemIcon>
{item.icon} {item.icon}
</ListItemIcon> </ListItemIcon>
@ -124,13 +139,20 @@ export default function DashboardLayout() {
React.useEffect(() => { React.useEffect(() => {
if (authStore) { if (authStore) {
const stored = authStore.getUserData() const stored = getUserData()
if (stored) { if (stored) {
setUserData(stored) setUserData(stored)
} }
} }
}, [authStore]) }, [authStore])
const getPageTitle = () => {
const currentPath = location.pathname;
const allPages = [...pages, ...misc];
const currentPage = allPages.find(page => page.path === currentPath);
return currentPage ? currentPage.label : "Dashboard";
};
return ( return (
<Box sx={{ <Box sx={{
display: 'flex', display: 'flex',
@ -156,7 +178,7 @@ export default function DashboardLayout() {
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
<Typography variant="h6" noWrap component="div"> <Typography variant="h6" noWrap component="div">
Dashboard {getPageTitle()}
</Typography> </Typography>
</Toolbar> </Toolbar>
</AppBar> </AppBar>

View File

@ -4,8 +4,6 @@ import { Outlet } from "react-router-dom";
export default function MainLayout() { export default function MainLayout() {
return ( return (
<> <Outlet />
<Outlet/>
</>
) )
} }

View File

@ -1,15 +1,14 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import UserService from "../services/UserService"
import AuthService from "../services/AuthService"
import { Button } from "@mui/material" import { Button } from "@mui/material"
import DataTable from "../components/DataTable" import DataTable from "../components/DataTable"
import { GridColDef } from "@mui/x-data-grid" import { GridColDef } from "@mui/x-data-grid"
import UserService from "../services/UserService"
export default function ApiTest() { export default function ApiTest() {
const [users, setUsers] = useState<any>(null) const [users, setUsers] = useState<any>(null)
const getUsers = async () => { const getUsers = async () => {
await AuthService.getUsers().then(response => { await UserService.getUsers().then(response => {
setUsers(response.data) setUsers(response.data)
}) })
} }

View File

@ -1,14 +1,14 @@
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import AuthService from "../services/AuthService"
import { Box, Button } from "@mui/material" import { Box, Button } from "@mui/material"
import DataTable from "../components/DataTable" import DataTable from "../components/DataTable"
import { GridColDef } from "@mui/x-data-grid" import { GridColDef } from "@mui/x-data-grid"
import UserService from "../services/UserService"
export default function Users() { export default function Users() {
const [users, setUsers] = useState<any>(null) const [users, setUsers] = useState<any>(null)
const getUsers = async () => { const getUsers = async () => {
await AuthService.getUsers().then(response => { await UserService.getUsers().then(response => {
setUsers(response.data) setUsers(response.data)
}) })
} }

View File

@ -1,12 +1,11 @@
import React, { useState, ChangeEvent, FormEvent } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material'; import { TextField, Button, Container, Typography, Box } from '@mui/material';
import axios, { AxiosResponse } from 'axios'; import { AxiosResponse } from 'axios';
import { SignInFormData, ApiResponse } from '../../types/auth'; import { SignInFormData, ApiResponse } from '../../interfaces/auth';
import { UserData, useAuthStore } from '../../store/auth'; import { login, setUserData } from '../../store/auth';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import axiosInstance from '../../http/axiosInstance';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import UserService from '../../services/UserService';
const SignIn = () => { const SignIn = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SignInFormData>({ const { register, handleSubmit, formState: { errors } } = useForm<SignInFormData>({
@ -20,7 +19,6 @@ const SignIn = () => {
} }
}) })
const authStore = useAuthStore();
const navigate = useNavigate(); const navigate = useNavigate();
const onSubmit: SubmitHandler<SignInFormData> = async (data) => { const onSubmit: SubmitHandler<SignInFormData> = async (data) => {
@ -30,21 +28,18 @@ const SignIn = () => {
} }
try { try {
const response: AxiosResponse<ApiResponse> = await axiosInstance.post(`/auth/login`, formBody, { const response: AxiosResponse<ApiResponse> = await AuthService.login(formBody)
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
console.log('Вход произошел успешно:', response.data); console.log('Вход произошел успешно:', response.data);
const token = response.data.access_token const token = response.data.access_token
const userDataResponse: AxiosResponse<ApiResponse> = await AuthService.getCurrentUser(token) const userDataResponse: AxiosResponse<ApiResponse> = await UserService.getCurrentUser(token)
console.log('Пользователь:', userDataResponse.data) console.log('Пользователь:', userDataResponse.data)
authStore.setUserData(JSON.stringify(userDataResponse.data)) setUserData(JSON.stringify(userDataResponse.data))
authStore.login(token)
login(token)
navigate('/'); navigate('/');
} catch (error) { } catch (error) {
@ -63,7 +58,8 @@ const SignIn = () => {
fullWidth fullWidth
margin="normal" margin="normal"
label="Логин" label="Логин"
{...register('username', { required: 'Логин обязателен' })} required
{...register('username', { required: 'Введите логин' })}
error={!!errors.username} error={!!errors.username}
helperText={errors.username?.message} helperText={errors.username?.message}
/> />
@ -72,7 +68,8 @@ const SignIn = () => {
margin="normal" margin="normal"
type="password" type="password"
label="Пароль" label="Пароль"
{...register('password', { required: 'Пароль обязателен' })} required
{...register('password', { required: 'Введите пароль' })}
error={!!errors.password} error={!!errors.password}
helperText={errors.password?.message} helperText={errors.password?.message}
/> />

View File

@ -1,9 +1,9 @@
import React, { useState, ChangeEvent, FormEvent } from 'react';
import { useForm, SubmitHandler } from 'react-hook-form'; import { useForm, SubmitHandler } from 'react-hook-form';
import { TextField, Button, Container, Typography, Box } from '@mui/material'; import { TextField, Button, Container, Typography, Box } from '@mui/material';
import axios, { AxiosResponse } from 'axios'; import axios, { AxiosResponse } from 'axios';
import { SignUpFormData, ApiResponse } from '../../types/auth'; import { SignUpFormData, ApiResponse } from '../../interfaces/auth';
import axiosInstance from '../../http/axiosInstance'; import axiosInstance from '../../http/axiosInstance';
import UserService from '../../services/UserService';
const SignUp = () => { const SignUp = () => {
const { register, handleSubmit, formState: { errors } } = useForm<SignUpFormData>({ const { register, handleSubmit, formState: { errors } } = useForm<SignUpFormData>({
@ -21,7 +21,7 @@ const SignUp = () => {
const onSubmit: SubmitHandler<SignUpFormData> = async (data) => { const onSubmit: SubmitHandler<SignUpFormData> = async (data) => {
try { try {
const response: AxiosResponse<ApiResponse> = await axiosInstance.post(`${import.meta.env.VITE_API_AUTH_URL}/auth/user`, data); const response: AxiosResponse<ApiResponse> = await UserService.createUser(data)
console.log('Успешная регистрация:', response.data); console.log('Успешная регистрация:', response.data);
} catch (error) { } catch (error) {
console.error('Ошибка регистрации:', error); console.error('Ошибка регистрации:', error);
@ -34,23 +34,28 @@ const SignUp = () => {
<Typography variant="h4" component="h1" gutterBottom> <Typography variant="h4" component="h1" gutterBottom>
Регистрация Регистрация
</Typography> </Typography>
<form onSubmit={handleSubmit(onSubmit)}> <form onSubmit={handleSubmit(onSubmit)}>
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
label="Email" label="Email"
required
{...register('email', { required: 'Email обязателен' })} {...register('email', { required: 'Email обязателен' })}
error={!!errors.email} error={!!errors.email}
helperText={errors.email?.message} helperText={errors.email?.message}
/> />
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
label="Логин" label="Логин"
required
{...register('login', { required: 'Логин обязателен' })} {...register('login', { required: 'Логин обязателен' })}
error={!!errors.login} error={!!errors.login}
helperText={errors.login?.message} helperText={errors.login?.message}
/> />
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
@ -59,6 +64,7 @@ const SignUp = () => {
error={!!errors.phone} error={!!errors.phone}
helperText={errors.phone?.message} helperText={errors.phone?.message}
/> />
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
@ -67,6 +73,7 @@ const SignUp = () => {
error={!!errors.name} error={!!errors.name}
helperText={errors.name?.message} helperText={errors.name?.message}
/> />
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
@ -75,15 +82,18 @@ const SignUp = () => {
error={!!errors.surname} error={!!errors.surname}
helperText={errors.surname?.message} helperText={errors.surname?.message}
/> />
<TextField <TextField
fullWidth fullWidth
margin="normal" margin="normal"
type="password" type="password"
label="Пароль" label="Пароль"
required
{...register('password', { required: 'Пароль обязателен' })} {...register('password', { required: 'Пароль обязателен' })}
error={!!errors.password} error={!!errors.password}
helperText={errors.password?.message} helperText={errors.password?.message}
/> />
<Button type="submit" variant="contained" color="primary"> <Button type="submit" variant="contained" color="primary">
Зарегистрироваться Зарегистрироваться
</Button> </Button>

View File

@ -1,16 +1,19 @@
import axios from "axios";
import axiosInstance from "../http/axiosInstance"; import axiosInstance from "../http/axiosInstance";
export default class AuthService { export default class AuthService {
static async hello() { static async login(data: any) {
return await axios.get(`${import.meta.env.VITE_API_AUTH_URL}/hello`) return await axiosInstance.post(`/auth/login`, data, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
} }
static async getUsers() { static async refreshToken(token: string) {
return await axiosInstance.get(`/auth/user`) return await axiosInstance.post(`/auth/refresh_token/${token}`)
} }
static async getCurrentUser(token: string){ static async getCurrentUser(token: string) {
return await axiosInstance.get(`/auth/get_current_user/${token}`) return await axiosInstance.get(`/auth/get_current_user/${token}`)
} }
} }

View File

@ -1,29 +1,32 @@
// Data mockup import axiosInstance from "../http/axiosInstance";
let users = import { UserCreds, UserData } from "../interfaces/auth";
[
{
"email": "string",
"login": "string",
"phone": "string",
"name": "string",
"surname": "string",
"is_active": true,
"id": 0,
"role_id": 2
}
]
export default class UserService { export default class UserService {
static async getUsers() { static async createUser(data: any) {
new Promise((resolve, reject) => { return await axiosInstance.post(`/auth/user`, data)
if (!users) { }
return setTimeout(
() => reject(new Error('Users not found')),
250
)
}
setTimeout(() => resolve(users), 250) static async getCurrentUser(token: string) {
}) return await axiosInstance.get(`/auth/get_current_user/${token}`)
}
static async getUsers() {
return await axiosInstance.get(`/auth/user`)
}
// static async deleteUser(id: number) {
// return await axiosInstance.delete(`/auth/user/${id}`)
// }
static async getUser(id: number) {
return await axiosInstance.get(`/auth/user/${id}`)
}
static async updatePassword(data: UserCreds) {
return await axiosInstance.put(`/auth/user/password_change`, data)
}
static async updateUser(data: UserData) {
return await axiosInstance.put(`/auth/user`, data)
} }
} }

View File

@ -1,50 +1,83 @@
import { create } from 'zustand'; import { create } from 'zustand';
import { TOKEN_AUTH_KEY, TOKEN_EXPIRY_DURATION, TOKEN_ISSUED_DATE_KEY, USER_DATA_KEY } from '../constants';
import { AuthState } from '../interfaces/auth';
import AuthService from '../services/AuthService';
export interface UserData { export const useAuthStore = create<AuthState>((set, get) => ({
id: number;
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
role_id: number;
}
interface AuthState {
isAuthenticated: boolean;
token: string | null;
login: (token: string) => void;
logout: () => void;
initializeAuth: () => void;
userData: UserData | {};
getUserData: () => UserData;
setUserData: (userData: string) => void;
}
export const useAuthStore = create<AuthState>((set) => ({
isAuthenticated: false, isAuthenticated: false,
token: null, token: null,
login: (token: string) => {
localStorage.setItem('authToken', token);
set({ isAuthenticated: true, token });
},
logout: () => {
localStorage.removeItem('authToken');
set({ isAuthenticated: false, token: null });
},
initializeAuth: () => {
const token = localStorage.getItem('authToken');
if (token) {
set({ isAuthenticated: true, token });
}
},
userData: {}, userData: {},
setUserData: (userData: string) => { }));
localStorage.setItem('userData', userData)
}, const login = (token: string) => {
getUserData: () => { const issuedDate = Date.now();
const userData = localStorage.getItem('userData') localStorage.setItem(TOKEN_AUTH_KEY, token);
return JSON.parse(userData || "") localStorage.setItem(TOKEN_ISSUED_DATE_KEY, issuedDate.toString());
useAuthStore.setState(() => ({ isAuthenticated: true, token: token }))
}
const logout = () => {
localStorage.removeItem(TOKEN_AUTH_KEY);
localStorage.removeItem(USER_DATA_KEY);
localStorage.removeItem(TOKEN_ISSUED_DATE_KEY);
useAuthStore.setState(() => ({ isAuthenticated: false, token: null, userData: {} }));
}
const initAuth = async () => {
const token = localStorage.getItem(TOKEN_AUTH_KEY);
const issuedDate = parseInt(localStorage.getItem(TOKEN_ISSUED_DATE_KEY) || '0', 10);
const currentTime = Date.now();
if (token && issuedDate) {
if (currentTime - issuedDate < TOKEN_EXPIRY_DURATION) {
useAuthStore.setState(() => ({ isAuthenticated: true, token: token }))
} else {
console.log("refreshing token")
try {
await refreshToken();
} catch (error) {
console.error('Token refresh failed:', error);
logout();
}
}
} else {
logout()
} }
})); }
const refreshToken = async () => {
const token = useAuthStore.getState().token
if (!token) throw new Error('No token to refresh');
try {
const response = await AuthService.refreshToken(token)
const newToken = response.data.access_token;
const newIssuedDate = Date.now();
localStorage.setItem(TOKEN_AUTH_KEY, newToken);
localStorage.setItem(TOKEN_ISSUED_DATE_KEY, newIssuedDate.toString());
useAuthStore.setState(() => ({ token: newToken }))
} catch (error) {
console.error('Failed to refresh token:', error);
logout();
}
}
const getUserData = () => {
const userData = localStorage.getItem(USER_DATA_KEY)
return userData ? JSON.parse(userData) : {}
}
const setUserData = (userData: string) => {
const parsedData = JSON.parse(userData)
localStorage.setItem(USER_DATA_KEY, userData)
useAuthStore.setState(() => ({ userData: parsedData }))
}
export {
getUserData,
setUserData,
refreshToken,
initAuth,
login,
logout
}