Auth: SignIn, SignUp (TODO: rewrite into react-hook-form)

This commit is contained in:
cracklesparkle
2024-06-24 17:06:41 +09:00
parent d6906503d1
commit 62695acf74
20 changed files with 617 additions and 71 deletions

View File

@ -1,4 +1,4 @@
import { BrowserRouter as Router, Route, Routes } from "react-router-dom"
import { BrowserRouter as Router, Route, Routes, Navigate, redirect } from "react-router-dom"
import Main from "./pages/Main"
import Users from "./pages/Users"
import Roles from "./pages/Roles"
@ -7,27 +7,52 @@ import DashboardLayout from "./layouts/DashboardLayout"
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 { useEffect, useState } from "react"
import { CircularProgress } from "@mui/material"
function App() {
return (
<>
<Router>
<Routes>
<Route element={<MainLayout/>}>
<Route path="/auth/signin" element={<SignIn/>}/>
</Route>
const auth = useAuthStore()
const [isLoading, setIsLoading] = useState(true)
<Route element={<DashboardLayout />}>
<Route path="/" element={<Main />} />
<Route path="/user" element={<Users />} />
<Route path="/role" element={<Roles />} />
<Route path="/api-test" element={<ApiTest />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
</>
)
useEffect(() => {
auth.initializeAuth()
}, [])
// Once auth is there, set loading to false and render the app
useEffect(() => {
if (auth) {
setIsLoading(false)
}
}, [auth])
if (isLoading) {
return (
<CircularProgress />
)
} else {
return (
<>
<Router>
<Routes>
<Route element={<MainLayout />}>
<Route path="/auth/signin" element={<SignIn />} />
<Route path="/auth/signup" element={<SignUp />} />
</Route>
<Route element={auth.isAuthenticated ? <DashboardLayout /> : <Navigate to={"/auth/signin"} />}>
<Route path="/" element={<Main />} />
<Route path="/user" element={<Users />} />
<Route path="/role" element={<Roles />} />
<Route path="/api-test" element={<ApiTest />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</Router>
</>
)
}
}
export default App

View File

@ -0,0 +1,24 @@
import { DataGrid } from '@mui/x-data-grid';
interface Props {
rows: any,
columns: any
}
export default function DataTable(props: Props) {
return (
<div style={{ flexGrow: 1, height: "100%", width: '100%' }}>
<DataGrid
rows={props.rows}
columns={props.columns}
initialState={{
pagination: {
paginationModel: { page: 0, pageSize: 5 },
},
}}
pageSizeOptions={[10, 20, 50, 100]}
checkboxSelection
/>
</div>
);
}

View File

@ -1,11 +1,12 @@
import { useState, useEffect, useMemo } from 'react';
import axiosInstance from '../http/axiosInstance';
export function useDataFetching<T>(url: string, initData: T): T {
const [data, setData] = useState<T>(initData);
useEffect(() => {
const fetchData = async () => {
const response = await fetch(url);
const result = await response.json();
const response = await axiosInstance.get(url);
const result = await response.data;
setData(result);
};

View File

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

View File

@ -0,0 +1,21 @@
import axios from 'axios';
import { useAuthStore } from '../store/auth';
const axiosInstance = axios.create({
baseURL: `${import.meta.env.VITE_API_AUTH_URL}`,
});
axiosInstance.interceptors.request.use(
(config) => {
const token = useAuthStore.getState().token;
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
export default axiosInstance;

View File

@ -1,6 +1,6 @@
// Layout for dashboard with responsive drawer
import { Outlet } from "react-router-dom"
import { Navigate, Outlet } from "react-router-dom"
import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Box from '@mui/material/Box';
@ -17,10 +17,14 @@ 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";
const drawerWidth = 240;
export default function DashboardLayout() {
const authStore = useAuthStore();
const [userData, setUserData] = React.useState<UserData>();
//const { window } = props;
const [mobileOpen, setMobileOpen] = React.useState(false);
const [isClosing, setIsClosing] = React.useState(false);
@ -78,7 +82,13 @@ export default function DashboardLayout() {
const drawer = (
<div>
<Toolbar />
<Toolbar>
<Box>
<Typography>{userData?.name} {userData?.surname}</Typography>
<Divider />
<Typography variant="caption">{userData?.login}</Typography>
</Box>
</Toolbar>
<Divider />
@ -112,8 +122,21 @@ export default function DashboardLayout() {
</div>
);
React.useEffect(() => {
if (authStore) {
const stored = authStore.getUserData()
if (stored) {
setUserData(stored)
}
}
}, [authStore])
return (
<Box sx={{ display: 'flex' }}>
<Box sx={{
display: 'flex',
flexGrow: 1,
height: "100vh"
}}>
<CssBaseline />
<AppBar
position="fixed"
@ -173,7 +196,11 @@ export default function DashboardLayout() {
<Box
component="main"
sx={{ flexGrow: 1, p: 3, width: { sm: `calc(100% - ${drawerWidth}px)` } }}
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` }
}}
>
<Toolbar />
<Outlet />

View File

@ -3,6 +3,18 @@ import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import './index.css'
import { registerSW } from 'virtual:pwa-register'
import { ThemeProvider } from '@emotion/react'
import { createTheme } from '@mui/material'
import { ruRU } from '@mui/material/locale'
const theme = createTheme(
{
palette: {
primary: { main: '#1976d2' },
},
},
ruRU,
);
const updateSW = registerSW({
onNeedRefresh() {
@ -17,6 +29,8 @@ const updateSW = registerSW({
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<App />
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</React.StrictMode>,
)

View File

@ -2,22 +2,43 @@ 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"
export default function ApiTest() {
const [temp, setTemp] = useState<any>(null)
const [users, setUsers] = useState<any>(null)
const hello = async () => {
await AuthService.hello().then(response => {
setTemp(response)
const getUsers = async () => {
await AuthService.getUsers().then(response => {
setUsers(response.data)
})
}
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'email', headerName: 'Email', width: 130 },
{ field: 'login', headerName: 'Логин', width: 130 },
{ field: 'phone', headerName: 'Телефон', width: 90 },
{ field: 'name', headerName: 'Имя', width: 90 },
{ field: 'surname', headerName: 'Фамилия', width: 90 },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90 },
{
field: 'role_id',
headerName: 'Роль',
valueGetter: (value, row) => `${value}`,
width: 90
},
];
return (
<>
<div>{JSON.stringify(temp)}</div>
<Button onClick={() => hello()}>
Hello
<Button onClick={() => getUsers()}>
Get users
</Button>
{users &&
<DataTable rows={users} columns={columns}/>
}
</>
)
}

View File

@ -2,6 +2,10 @@ import { useState } from 'react'
import RoleCard from '../components/RoleCard'
import Modal from '../components/Modal'
import useDataFetching from '../components/FetchingData'
import RoleService from '../services/RoleService'
import { Box, Button } from '@mui/material'
import DataTable from '../components/DataTable'
import { GridColDef } from '@mui/x-data-grid'
interface IRoleCard {
id: number
@ -11,12 +15,38 @@ interface Props {
showModal: boolean;
}
function Roles() {
const [roles, setRoles] = useState<any>(null)
const getRoles = async () => {
await RoleService.getRoles().then(response => {
setRoles(response.data)
})
}
const [showModal, setShowModal] = useState<Props>({ showModal: false });
const cards = useDataFetching<IRoleCard[]>(`${import.meta.env.VITE_API_URL}/auth/role/`, [])
const cards = useDataFetching<IRoleCard[]>(`${import.meta.env.VITE_API_AUTH_URL}/auth/roles/`, [])
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'name', headerName: 'Название', width: 90 },
{ field: 'description', headerName: 'Описание', width: 90 },
];
return (
<Box>
<Button onClick={() => getRoles()}>
Get roles
</Button>
{roles &&
<DataTable rows={roles} columns={columns} />
}
</Box>
)
return (
<div>
{cards.map((card, index) => <RoleCard key={index} {...card} />)}
{cards.length > 0 && cards.map((card, index) => <RoleCard key={index} {...card} />)}
<button className='absolute w-0 h-0' onClick={() => setShowModal({ showModal: true })}>+</button>
<Modal {...showModal} />
</div>

View File

@ -1,18 +1,43 @@
import Card from '../components/Card'
import useDataFetching from '../components/FetchingData'
interface ICard {
firstname: string
lastname: string
email: string
}
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"
function Users() {
const cards = useDataFetching<ICard[]>(`${import.meta.env.VITE_API_URL}/auth/user/`, [])
return (
<div>
{cards.map((card, index) => <Card key={index} {...card} />)}
</div>
)
}
export default function Users() {
const [users, setUsers] = useState<any>(null)
export default Users
const getUsers = async () => {
await AuthService.getUsers().then(response => {
setUsers(response.data)
})
}
const columns: GridColDef[] = [
{ field: 'id', headerName: 'ID', type: "number", width: 70 },
{ field: 'email', headerName: 'Email', width: 130 },
{ field: 'login', headerName: 'Логин', width: 130 },
{ field: 'phone', headerName: 'Телефон', width: 90 },
{ field: 'name', headerName: 'Имя', width: 90 },
{ field: 'surname', headerName: 'Фамилия', width: 90 },
{ field: 'is_active', headerName: 'Активен', type: "boolean", width: 90 },
{
field: 'role_id',
headerName: 'Роль',
valueGetter: (value, row) => `${value}`,
width: 90
},
];
return (
<Box>
<Button onClick={() => getUsers()}>
Get users
</Button>
{users &&
<DataTable rows={users} columns={columns}/>
}
</Box>
)
}

View File

@ -1,9 +1,99 @@
import React from 'react'
import React, { useState, ChangeEvent, FormEvent } from 'react';
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 { useNavigate } from 'react-router-dom';
import axiosInstance from '../../http/axiosInstance';
import AuthService from '../../services/AuthService';
const SignIn = () => {
return (
<div>SignIn</div>
)
}
const [formData, setFormData] = useState<SignInFormData>({
username: '',
password: '',
grant_type: 'password',
scope: '',
client_id: '',
client_secret: ''
});
export default SignIn
const authStore = useAuthStore();
const navigate = useNavigate();
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
const formBody = new URLSearchParams();
for (const key in formData) {
formBody.append(key, formData[key as keyof SignInFormData] as string);
}
try {
const response: AxiosResponse<ApiResponse> = await axiosInstance.post(`/auth/login`, formBody, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
});
console.log('Вход произошел успешно:', response.data);
const token = response.data.access_token
const userDataResponse: AxiosResponse<ApiResponse> = await AuthService.getCurrentUser(token)
console.log('Пользователь:', userDataResponse.data)
authStore.setUserData(JSON.stringify(userDataResponse.data))
authStore.login(token)
navigate('/');
} catch (error) {
console.error('Ошибка при входе:', error);
}
};
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
Вход
</Typography>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
margin="normal"
name="username"
label="Логин"
value={formData.username}
onChange={handleChange}
required
/>
<TextField
fullWidth
margin="normal"
type="password"
name="password"
label="Пароль"
value={formData.password}
onChange={handleChange}
required
/>
<Button type="submit" variant="contained" color="primary">
Вход
</Button>
<Button href="/auth/signup" type="button" variant="text" color="primary">
Регистрация
</Button>
</form>
</Box>
</Container>
);
};
export default SignIn;

View File

@ -0,0 +1,105 @@
// src/components/SignUp.tsx
import React, { useState, ChangeEvent, FormEvent } from 'react';
import { TextField, Button, Container, Typography, Box } from '@mui/material';
import axios, { AxiosResponse } from 'axios';
import { SignUpFormData, ApiResponse } from '../../types/auth';
import axiosInstance from '../../http/axiosInstance';
const SignUp: React.FC = () => {
const [formData, setFormData] = useState<SignUpFormData>({
email: '',
login: '',
phone: '',
name: '',
surname: '',
is_active: true,
password: '',
});
const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
setFormData({
...formData,
[e.target.name]: e.target.value,
});
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
try {
const response: AxiosResponse<ApiResponse> = await axiosInstance.post(`${import.meta.env.VITE_API_AUTH_URL}/auth/user`, formData);
console.log('Успешная регистрация:', response.data);
} catch (error) {
console.error('Ошибка регистрации:', error);
}
};
return (
<Container maxWidth="sm">
<Box my={4}>
<Typography variant="h4" component="h1" gutterBottom>
Регистрация
</Typography>
<form onSubmit={handleSubmit}>
<TextField
fullWidth
margin="normal"
name="email"
label="Email"
value={formData.email}
onChange={handleChange}
required
/>
<TextField
fullWidth
margin="normal"
name="login"
label="Логин"
value={formData.login}
onChange={handleChange}
required
/>
<TextField
fullWidth
margin="normal"
name="phone"
label="Телефон"
value={formData.phone}
onChange={handleChange}
/>
<TextField
fullWidth
margin="normal"
name="name"
label="Имя"
value={formData.name}
onChange={handleChange}
/>
<TextField
fullWidth
margin="normal"
name="surname"
label="Фамилия"
value={formData.surname}
onChange={handleChange}
/>
<TextField
fullWidth
margin="normal"
type="password"
name="password"
label="Пароль"
value={formData.password}
onChange={handleChange}
required
/>
<Button type="submit" variant="contained" color="primary">
Зарегистрироваться
</Button>
</form>
</Box>
</Container>
);
};
export default SignUp;

View File

@ -1,7 +1,16 @@
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 getUsers() {
return await axiosInstance.get(`/auth/user`)
}
static async getCurrentUser(token: string){
return await axiosInstance.get(`/auth/get_current_user/${token}`)
}
}

View File

@ -0,0 +1,11 @@
import axiosInstance from "../http/axiosInstance";
export default class RoleService {
static async getRoles() {
return await axiosInstance.get(`/auth/roles`)
}
static async getRoleById(id: number) {
return await axiosInstance.get(`/auth/roles/${id}`)
}
}

View File

@ -0,0 +1,50 @@
import { create } from 'zustand';
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<AuthState>((set) => ({
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 || "")
}
}));

View File

@ -0,0 +1,25 @@
export interface SignUpFormData {
email: string;
login: string;
phone: string;
name: string;
surname: string;
is_active: boolean;
password: string;
}
export interface SignInFormData {
username: string;
password: string;
grant_type: string;
scope?: string;
client_id?: string;
client_secret?: string;
}
export interface ApiResponse {
access_token: any;
data: any;
status: number;
statusText: string;
}