From 18fb1207774413952b60e2705e5b433d34137946 Mon Sep 17 00:00:00 2001 From: cracklesparkle Date: Tue, 25 Jun 2024 15:56:00 +0900 Subject: [PATCH] Refactored store --- frontend_reactjs/src/App.tsx | 4 +- frontend_reactjs/src/components/DataTable.tsx | 2 +- frontend_reactjs/src/components/UserData.ts | 3 +- frontend_reactjs/src/constants/index.ts | 4 + frontend_reactjs/src/http/axiosInstance.ts | 2 +- .../src/{types => interfaces}/auth.ts | 24 ++++ .../src/layouts/DashboardLayout.tsx | 34 ++++- frontend_reactjs/src/layouts/MainLayout.tsx | 4 +- frontend_reactjs/src/pages/ApiTest.tsx | 5 +- frontend_reactjs/src/pages/Users.tsx | 4 +- frontend_reactjs/src/pages/auth/SignIn.tsx | 29 ++--- frontend_reactjs/src/pages/auth/SignUp.tsx | 16 ++- frontend_reactjs/src/services/AuthService.ts | 15 ++- frontend_reactjs/src/services/UserService.ts | 51 ++++---- frontend_reactjs/src/store/auth.ts | 121 +++++++++++------- 15 files changed, 205 insertions(+), 113 deletions(-) rename frontend_reactjs/src/{types => interfaces}/auth.ts (53%) diff --git a/frontend_reactjs/src/App.tsx b/frontend_reactjs/src/App.tsx index 5958b75..88522c4 100644 --- a/frontend_reactjs/src/App.tsx +++ b/frontend_reactjs/src/App.tsx @@ -8,7 +8,7 @@ import MainLayout from "./layouts/MainLayout" import SignIn from "./pages/auth/SignIn" import ApiTest from "./pages/ApiTest" import SignUp from "./pages/auth/SignUp" -import { useAuthStore } from "./store/auth" +import { initAuth, useAuthStore } from "./store/auth" import { useEffect, useState } from "react" import { CircularProgress } from "@mui/material" @@ -17,7 +17,7 @@ function App() { const [isLoading, setIsLoading] = useState(true) useEffect(() => { - auth.initializeAuth() + initAuth() }, []) // Once auth is there, set loading to false and render the app diff --git a/frontend_reactjs/src/components/DataTable.tsx b/frontend_reactjs/src/components/DataTable.tsx index c32b05e..219a736 100644 --- a/frontend_reactjs/src/components/DataTable.tsx +++ b/frontend_reactjs/src/components/DataTable.tsx @@ -13,7 +13,7 @@ export default function DataTable(props: Props) { columns={props.columns} initialState={{ pagination: { - paginationModel: { page: 0, pageSize: 5 }, + paginationModel: { page: 0, pageSize: 10 }, }, }} pageSizeOptions={[10, 20, 50, 100]} diff --git a/frontend_reactjs/src/components/UserData.ts b/frontend_reactjs/src/components/UserData.ts index a83ff95..5700cd1 100644 --- a/frontend_reactjs/src/components/UserData.ts +++ b/frontend_reactjs/src/components/UserData.ts @@ -1,13 +1,12 @@ import { memo, useEffect, useMemo, useState } from "react"; import UserService from "../services/UserService"; -import AuthService from "../services/AuthService"; export default function useUserData(token: string, initData: T): T { const [userData, setUserData] = useState(initData) useEffect(()=> { const fetchUserData = async (token: string) => { - const response = await AuthService.getCurrentUser(token) + const response = await UserService.getCurrentUser(token) setUserData(response.data) } diff --git a/frontend_reactjs/src/constants/index.ts b/frontend_reactjs/src/constants/index.ts index e69de29..a5bb984 100644 --- a/frontend_reactjs/src/constants/index.ts +++ b/frontend_reactjs/src/constants/index.ts @@ -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; \ No newline at end of file diff --git a/frontend_reactjs/src/http/axiosInstance.ts b/frontend_reactjs/src/http/axiosInstance.ts index 671dee2..3cc3eb6 100644 --- a/frontend_reactjs/src/http/axiosInstance.ts +++ b/frontend_reactjs/src/http/axiosInstance.ts @@ -18,4 +18,4 @@ axiosInstance.interceptors.request.use( } ); -export default axiosInstance; +export default axiosInstance; \ No newline at end of file diff --git a/frontend_reactjs/src/types/auth.ts b/frontend_reactjs/src/interfaces/auth.ts similarity index 53% rename from frontend_reactjs/src/types/auth.ts rename to frontend_reactjs/src/interfaces/auth.ts index ac1bb4a..c4ac87f 100644 --- a/frontend_reactjs/src/types/auth.ts +++ b/frontend_reactjs/src/interfaces/auth.ts @@ -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 { email: string; login: string; diff --git a/frontend_reactjs/src/layouts/DashboardLayout.tsx b/frontend_reactjs/src/layouts/DashboardLayout.tsx index 0bb7296..350624b 100644 --- a/frontend_reactjs/src/layouts/DashboardLayout.tsx +++ b/frontend_reactjs/src/layouts/DashboardLayout.tsx @@ -1,6 +1,6 @@ // 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 AppBar from '@mui/material/AppBar'; import Box from '@mui/material/Box'; @@ -17,7 +17,8 @@ import MenuIcon from '@mui/icons-material/Menu'; import Toolbar from '@mui/material/Toolbar'; import Typography from '@mui/material/Typography'; 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; @@ -25,6 +26,10 @@ export default function DashboardLayout() { const authStore = useAuthStore(); const [userData, setUserData] = React.useState(); + const location = useLocation() + + const navigate = useNavigate() + //const { window } = props; const [mobileOpen, setMobileOpen] = React.useState(false); const [isClosing, setIsClosing] = React.useState(false); @@ -95,7 +100,12 @@ export default function DashboardLayout() { {pages.map((item, index) => ( - + { + navigate(item.path) + }} + selected={location.pathname === item.path} + > {item.icon} @@ -110,7 +120,12 @@ export default function DashboardLayout() { {misc.map((item, index) => ( - + { + navigate(item.path) + }} + selected={location.pathname === item.path} + > {item.icon} @@ -124,13 +139,20 @@ export default function DashboardLayout() { React.useEffect(() => { if (authStore) { - const stored = authStore.getUserData() + const stored = getUserData() if (stored) { setUserData(stored) } } }, [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 ( - Dashboard + {getPageTitle()} diff --git a/frontend_reactjs/src/layouts/MainLayout.tsx b/frontend_reactjs/src/layouts/MainLayout.tsx index 5bf54c2..96845e3 100644 --- a/frontend_reactjs/src/layouts/MainLayout.tsx +++ b/frontend_reactjs/src/layouts/MainLayout.tsx @@ -4,8 +4,6 @@ import { Outlet } from "react-router-dom"; export default function MainLayout() { return ( - <> - - + ) } \ No newline at end of file diff --git a/frontend_reactjs/src/pages/ApiTest.tsx b/frontend_reactjs/src/pages/ApiTest.tsx index 10a1f6a..9b1eba3 100644 --- a/frontend_reactjs/src/pages/ApiTest.tsx +++ b/frontend_reactjs/src/pages/ApiTest.tsx @@ -1,15 +1,14 @@ import { useEffect, useState } from "react" -import UserService from "../services/UserService" -import AuthService from "../services/AuthService" import { Button } from "@mui/material" import DataTable from "../components/DataTable" import { GridColDef } from "@mui/x-data-grid" +import UserService from "../services/UserService" export default function ApiTest() { const [users, setUsers] = useState(null) const getUsers = async () => { - await AuthService.getUsers().then(response => { + await UserService.getUsers().then(response => { setUsers(response.data) }) } diff --git a/frontend_reactjs/src/pages/Users.tsx b/frontend_reactjs/src/pages/Users.tsx index ae3dee5..32c6664 100644 --- a/frontend_reactjs/src/pages/Users.tsx +++ b/frontend_reactjs/src/pages/Users.tsx @@ -1,14 +1,14 @@ import { useEffect, useState } from "react" -import AuthService from "../services/AuthService" import { Box, Button } from "@mui/material" import DataTable from "../components/DataTable" import { GridColDef } from "@mui/x-data-grid" +import UserService from "../services/UserService" export default function Users() { const [users, setUsers] = useState(null) const getUsers = async () => { - await AuthService.getUsers().then(response => { + await UserService.getUsers().then(response => { setUsers(response.data) }) } diff --git a/frontend_reactjs/src/pages/auth/SignIn.tsx b/frontend_reactjs/src/pages/auth/SignIn.tsx index 275d698..bb4ebb7 100644 --- a/frontend_reactjs/src/pages/auth/SignIn.tsx +++ b/frontend_reactjs/src/pages/auth/SignIn.tsx @@ -1,12 +1,11 @@ -import React, { useState, ChangeEvent, FormEvent } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { TextField, Button, Container, Typography, Box } from '@mui/material'; -import axios, { AxiosResponse } from 'axios'; -import { SignInFormData, ApiResponse } from '../../types/auth'; -import { UserData, useAuthStore } from '../../store/auth'; +import { AxiosResponse } from 'axios'; +import { SignInFormData, ApiResponse } from '../../interfaces/auth'; +import { login, setUserData } from '../../store/auth'; import { useNavigate } from 'react-router-dom'; -import axiosInstance from '../../http/axiosInstance'; import AuthService from '../../services/AuthService'; +import UserService from '../../services/UserService'; const SignIn = () => { const { register, handleSubmit, formState: { errors } } = useForm({ @@ -20,7 +19,6 @@ const SignIn = () => { } }) - const authStore = useAuthStore(); const navigate = useNavigate(); const onSubmit: SubmitHandler = async (data) => { @@ -30,21 +28,18 @@ const SignIn = () => { } try { - const response: AxiosResponse = await axiosInstance.post(`/auth/login`, formBody, { - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); + const response: AxiosResponse = await AuthService.login(formBody) console.log('Вход произошел успешно:', response.data); const token = response.data.access_token - const userDataResponse: AxiosResponse = await AuthService.getCurrentUser(token) + const userDataResponse: AxiosResponse = await UserService.getCurrentUser(token) console.log('Пользователь:', userDataResponse.data) - authStore.setUserData(JSON.stringify(userDataResponse.data)) - authStore.login(token) + setUserData(JSON.stringify(userDataResponse.data)) + + login(token) navigate('/'); } catch (error) { @@ -63,7 +58,8 @@ const SignIn = () => { fullWidth margin="normal" label="Логин" - {...register('username', { required: 'Логин обязателен' })} + required + {...register('username', { required: 'Введите логин' })} error={!!errors.username} helperText={errors.username?.message} /> @@ -72,7 +68,8 @@ const SignIn = () => { margin="normal" type="password" label="Пароль" - {...register('password', { required: 'Пароль обязателен' })} + required + {...register('password', { required: 'Введите пароль' })} error={!!errors.password} helperText={errors.password?.message} /> diff --git a/frontend_reactjs/src/pages/auth/SignUp.tsx b/frontend_reactjs/src/pages/auth/SignUp.tsx index 9491d65..6f81336 100644 --- a/frontend_reactjs/src/pages/auth/SignUp.tsx +++ b/frontend_reactjs/src/pages/auth/SignUp.tsx @@ -1,9 +1,9 @@ -import React, { useState, ChangeEvent, FormEvent } from 'react'; import { useForm, SubmitHandler } from 'react-hook-form'; import { TextField, Button, Container, Typography, Box } from '@mui/material'; import axios, { AxiosResponse } from 'axios'; -import { SignUpFormData, ApiResponse } from '../../types/auth'; +import { SignUpFormData, ApiResponse } from '../../interfaces/auth'; import axiosInstance from '../../http/axiosInstance'; +import UserService from '../../services/UserService'; const SignUp = () => { const { register, handleSubmit, formState: { errors } } = useForm({ @@ -21,7 +21,7 @@ const SignUp = () => { const onSubmit: SubmitHandler = async (data) => { try { - const response: AxiosResponse = await axiosInstance.post(`${import.meta.env.VITE_API_AUTH_URL}/auth/user`, data); + const response: AxiosResponse = await UserService.createUser(data) console.log('Успешная регистрация:', response.data); } catch (error) { console.error('Ошибка регистрации:', error); @@ -34,23 +34,28 @@ const SignUp = () => { Регистрация +
+ + { error={!!errors.phone} helperText={errors.phone?.message} /> + { error={!!errors.name} helperText={errors.name?.message} /> + { error={!!errors.surname} helperText={errors.surname?.message} /> + + diff --git a/frontend_reactjs/src/services/AuthService.ts b/frontend_reactjs/src/services/AuthService.ts index 1c38b3c..6966a54 100644 --- a/frontend_reactjs/src/services/AuthService.ts +++ b/frontend_reactjs/src/services/AuthService.ts @@ -1,16 +1,19 @@ -import axios from "axios"; import axiosInstance from "../http/axiosInstance"; export default class AuthService { - static async hello() { - return await axios.get(`${import.meta.env.VITE_API_AUTH_URL}/hello`) + static async login(data: any) { + return await axiosInstance.post(`/auth/login`, data, { + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + } + }) } - static async getUsers() { - return await axiosInstance.get(`/auth/user`) + static async refreshToken(token: string) { + 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}`) } } \ No newline at end of file diff --git a/frontend_reactjs/src/services/UserService.ts b/frontend_reactjs/src/services/UserService.ts index f00a54f..abdb844 100644 --- a/frontend_reactjs/src/services/UserService.ts +++ b/frontend_reactjs/src/services/UserService.ts @@ -1,29 +1,32 @@ -// Data mockup -let users = - [ - { - "email": "string", - "login": "string", - "phone": "string", - "name": "string", - "surname": "string", - "is_active": true, - "id": 0, - "role_id": 2 - } - ] +import axiosInstance from "../http/axiosInstance"; +import { UserCreds, UserData } from "../interfaces/auth"; export default class UserService { - static async getUsers() { - new Promise((resolve, reject) => { - if (!users) { - return setTimeout( - () => reject(new Error('Users not found')), - 250 - ) - } + static async createUser(data: any) { + return await axiosInstance.post(`/auth/user`, data) + } - 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) } } \ No newline at end of file diff --git a/frontend_reactjs/src/store/auth.ts b/frontend_reactjs/src/store/auth.ts index 9086e14..2e07a82 100644 --- a/frontend_reactjs/src/store/auth.ts +++ b/frontend_reactjs/src/store/auth.ts @@ -1,50 +1,83 @@ 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 { - 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((set) => ({ +export const useAuthStore = create((set, get) => ({ isAuthenticated: false, 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: {}, - setUserData: (userData: string) => { - localStorage.setItem('userData', userData) - }, - getUserData: () => { - const userData = localStorage.getItem('userData') - return JSON.parse(userData || "") +})); + +const login = (token: string) => { + const issuedDate = Date.now(); + localStorage.setItem(TOKEN_AUTH_KEY, token); + 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() } -})); \ No newline at end of file +} + +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 +} \ No newline at end of file