fix authentication

This commit is contained in:
nflnkr 2023-05-17 14:20:11 +03:00
parent e6d963fc5a
commit f98f0b4d2f
12 changed files with 344 additions and 255 deletions

51
src/api/auth.ts Normal file

@ -0,0 +1,51 @@
import { CreateUserRequest, User } from "@root/model/auth";
import { authStore } from "@root/stores/makeRequest";
const apiUrl = process.env.NODE_ENV === "production" ? "/user" : "https://hub.pena.digital/user";
const authUrl = process.env.NODE_ENV === "production" ? "/auth" : "https://hub.pena.digital/auth";
const makeRequest = authStore.getState().makeRequest;
export async function getOrCreateUser({ userId, email, password }: {
userId: string;
email: string | null;
password: string | null;
}): Promise<User | null> {
try {
return await makeRequest<never, User>({
url: `${apiUrl}/${userId}`,
contentType: true,
method: "GET",
useToken: false,
withCredentials: false,
});
} catch (error) {
console.log("get user error", error);
if (!email || !password) return null;
return makeRequest<CreateUserRequest, User>({
url: apiUrl,
contentType: true,
method: "POST",
useToken: true,
withCredentials: false,
body: {
email,
login: email,
password,
phoneNumber: "-",
}
});
}
}
export function logout() {
return makeRequest<never, void>({
url: authUrl + "/logout",
method: "POST",
useToken: false,
withCredentials: true,
});
}

13
src/components/Layout.tsx Normal file

@ -0,0 +1,13 @@
import { Outlet } from "react-router-dom";
import Navbar from "./Navbar/Navbar";
export default function Layout() {
return (
<>
<Navbar isLoggedIn={true} />
<Outlet />
</>
);
}

@ -1,11 +1,15 @@
import { useMediaQuery, useTheme } from "@mui/material";
import NavbarCollapsed from "./NavbarCollapsed"; import NavbarCollapsed from "./NavbarCollapsed";
import NavbarFull from "./NavbarFull"; import NavbarFull from "./NavbarFull";
interface Props { interface Props {
isLoggedIn: boolean; isCollapsed?: boolean;
isCollapsed?: boolean; isLoggedIn: boolean;
} }
export default function Navbar({ isLoggedIn, isCollapsed = false }: Props) { export default function Navbar({ isLoggedIn, isCollapsed = false }: Props) {
return isCollapsed ? <NavbarCollapsed isLoggedIn={isLoggedIn} /> : <NavbarFull isLoggedIn={isLoggedIn} />; const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
return upMd ? <NavbarFull isLoggedIn={isLoggedIn} /> : <NavbarCollapsed isLoggedIn={isLoggedIn} />;
} }

@ -1,38 +1,41 @@
import { IconButton, useTheme } from "@mui/material"; import { Divider, IconButton, useTheme } from "@mui/material";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import MenuIcon from "@mui/icons-material/Menu"; import MenuIcon from "@mui/icons-material/Menu";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
} }
export default function NavbarCollapsed({ isLoggedIn }: Props) { export default function NavbarCollapsed({ isLoggedIn }: Props) {
const theme = useTheme(); const theme = useTheme();
return ( return (
<SectionWrapper <>
component="nav" <SectionWrapper
maxWidth="lg" component="nav"
outerContainerSx={{ maxWidth="lg"
backgroundColor: theme.palette.navbarbg.main, outerContainerSx={{
position: "sticky", backgroundColor: theme.palette.navbarbg.main,
top: 0, position: "sticky",
zIndex: 1, top: 0,
// borderBottom: "1px solid #E3E3E3", zIndex: 1,
}} // borderBottom: "1px solid #E3E3E3",
sx={{ }}
height: "51px", sx={{
py: "6px", height: "51px",
display: "flex", py: "6px",
justifyContent: "space-between", display: "flex",
alignItems: "center", justifyContent: "space-between",
}} alignItems: "center",
> }}
<PenaLogo width={100} /> >
<IconButton sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}> <PenaLogo width={100} />
<MenuIcon sx={{ height: "30px", width: "30px" }} /> <IconButton sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}>
</IconButton> <MenuIcon sx={{ height: "30px", width: "30px" }} />
</SectionWrapper> </IconButton>
); </SectionWrapper>
{!isLoggedIn && <Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} />}
</>
);
} }

@ -1,6 +1,6 @@
import { Link, useLocation } from "react-router-dom"; import { Link, useLocation, useNavigate } from "react-router-dom";
import { useEffect } from "react"; import { useEffect } from "react";
import { Box, Button, Container, IconButton, Typography, useTheme } from "@mui/material"; import { Box, Button, Container, Divider, IconButton, Typography, useTheme } from "@mui/material";
import SectionWrapper from "../SectionWrapper"; import SectionWrapper from "../SectionWrapper";
import { basketStore } from "@stores/BasketStore"; import { basketStore } from "@stores/BasketStore";
@ -12,6 +12,9 @@ import CustomAvatar from "./Avatar";
import Drawers from "../Drawers"; import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo"; import PenaLogo from "../PenaLogo";
import Menu from "../Menu"; import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUser, useUserStore } from "@root/stores/user";
interface Props { interface Props {
isLoggedIn: boolean; isLoggedIn: boolean;
@ -21,6 +24,8 @@ export default function NavbarFull({ isLoggedIn }: Props) {
const theme = useTheme(); const theme = useTheme();
const { clearToken } = authStore(); const { clearToken } = authStore();
const location = useLocation(); const location = useLocation();
const navigate = useNavigate();
const user = useUserStore(state => state.user);
const { open } = basketStore(); const { open } = basketStore();
@ -31,7 +36,15 @@ export default function NavbarFull({ isLoggedIn }: Props) {
}, [location.pathname, open]); }, [location.pathname, open]);
async function handleLogoutClick() { async function handleLogoutClick() {
clearToken(); try {
await logout();
clearToken();
clearUser();
navigate("/");
} catch (error: any) {
console.log("Logout error", error);
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Logout error");
}
} }
return isLoggedIn ? ( return isLoggedIn ? (
@ -85,40 +98,43 @@ export default function NavbarFull({ isLoggedIn }: Props) {
</Box> </Box>
</Container> </Container>
) : ( ) : (
<SectionWrapper <>
component="nav" <SectionWrapper
maxWidth="lg" component="nav"
outerContainerSx={{ maxWidth="lg"
backgroundColor: theme.palette.lightPurple.main, outerContainerSx={{
// borderBottom: "1px solid #E3E3E3", backgroundColor: theme.palette.lightPurple.main,
}} // borderBottom: "1px solid #E3E3E3",
sx={{ }}
px: "20px",
display: "flex",
justifyContent: "space-between",
height: "80px",
alignItems: "center",
gap: "50px",
}}
>
<PenaLogo width={150} />
<Menu />
<Button
component={Link}
to="/signin"
state={{ backgroundLocation: location }}
variant="outlined"
sx={{ sx={{
px: "18px", px: "20px",
py: "10px", display: "flex",
borderColor: "white", justifyContent: "space-between",
borderRadius: "8px", height: "80px",
whiteSpace: "nowrap", alignItems: "center",
minWidth: "180px", gap: "50px",
}} }}
> >
Личный кабинет <PenaLogo width={150} />
</Button> <Menu />
</SectionWrapper> <Button
component={Link}
to={user ? "/tariffs" : "/signin"}
state={user ? undefined : { backgroundLocation: location }}
variant="outlined"
sx={{
px: "18px",
py: "10px",
borderColor: "white",
borderRadius: "8px",
whiteSpace: "nowrap",
minWidth: "180px",
}}
>
Личный кабинет
</Button>
</SectionWrapper>
<Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} />
</>
); );
} }

@ -1,7 +1,7 @@
import React from "react"; import React, { useEffect } from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import { CssBaseline, Divider, ThemeProvider, useMediaQuery } from "@mui/material"; import { CssBaseline, ThemeProvider } from "@mui/material";
import Faq from "./pages/Faq/Faq"; import Faq from "./pages/Faq/Faq";
import Wallet from "./pages/Wallet"; import Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment"; import Payment from "./pages/Payment/Payment";
@ -15,23 +15,34 @@ import SignupDialog from "./pages/auth/Signup";
import PaymentHistory from "./pages/PaymentHistory/PaymentHistory"; import PaymentHistory from "./pages/PaymentHistory/PaymentHistory";
import Basket from "./pages/Basket/Basket"; import Basket from "./pages/Basket/Basket";
import TariffPage from "./pages/Tariffs/TariffsPage"; import TariffPage from "./pages/Tariffs/TariffsPage";
import Footer from "@components/Footer";
import Navbar from "@components/Navbar/Navbar";
import darkTheme from "@utils/themes/dark";
import lightTheme from "@utils/themes/light"; import lightTheme from "@utils/themes/light";
import PublicRoute from "@utils/routes/publicRoute";
import PrivateRoute from "@utils/routes/privateRoute"; import PrivateRoute from "@utils/routes/privateRoute";
import reportWebVitals from "./reportWebVitals"; import reportWebVitals from "./reportWebVitals";
import { SnackbarProvider } from "notistack"; import { SnackbarProvider, enqueueSnackbar } from "notistack";
import "./index.css"; import "./index.css";
import Layout from "./components/Layout";
import { getOrCreateUser } from "./api/auth";
import { setUser, useUserStore } from "./stores/user";
// TODO refactor routes
const App = () => { const App = () => {
const location = useLocation(); const location = useLocation();
const upMd = useMediaQuery(lightTheme.breakpoints.up("md")); const userId = useUserStore(state => state.userId);
const email = useUserStore(state => state.email);
const password = useUserStore(state => state.password);
const state = location.state as { backgroundLocation?: Location; }; useEffect(function fetchUserData() {
if (!userId) return;
getOrCreateUser({ userId, email, password }).then(result => {
setUser(result);
}).catch(error => {
console.log("Error fetching user", error);
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Error fetching user");
});
}, [email, password, userId]);
const state = (location.state as { backgroundLocation?: Location; });
return ( return (
<> <>
@ -42,133 +53,23 @@ const App = () => {
</Routes> </Routes>
} }
<Routes location={state?.backgroundLocation || location}> <Routes location={state?.backgroundLocation || location}>
<Route <Route path="/" element={<Landing />} />
path="/" <Route element={<Layout />}>
element={ <Route path="/tariffs" element={<Tariffs />} />
<ThemeProvider theme={darkTheme}> <Route path="/tariffs/time" element={<TariffPage />} />
<CssBaseline /> <Route path="/tariffs/volume" element={<TariffPage />} />
<Navbar isLoggedIn={false} isCollapsed={!upMd} /> <Route path="/faq" element={<Faq />} />
<Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} /> <Route path="/support" element={<Support />} />
<Landing /> <Route path="/support/:ticketId" element={<Support />} />
<Footer /> <Route path="/tariffconstructor" element={<CustomTariff />} />
</ThemeProvider> <Route path="/basket" element={<Basket />} />
} <Route element={<PrivateRoute />}>
/> <Route path="/wallet" element={<Wallet />} />
<Route <Route path="/payment" element={<Payment />} />
path="tariffs" <Route path="/settings" element={<AccountSetup />} />
element={ <Route path="/paymenthistory" element={<PaymentHistory />} />
<> </Route>
<Navbar isLoggedIn={true} isCollapsed={!upMd} /> </Route>
<Tariffs />
</>
}
/>
<Route
path="tariffs/time"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<TariffPage />
</>
} />
<Route
path="tariffs/volume"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<TariffPage />
</>
} />
<Route
path="/faq"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Faq />
</>
}
/>
<Route
path="/wallet"
element={
// <PrivateRoute>
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Wallet />
</>
// </PrivateRoute>
}
/>
<Route
path="/payment"
element={
// <PrivateRoute>
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Payment />
</>
// </PrivateRoute>
}
/>
<Route
path="/support"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/support/:ticketId"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Support />
</>
}
/>
<Route
path="/tariffconstructor"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<CustomTariff />
</>
}
/>
<Route
path="/settings"
element={
// <PrivateRoute>
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<AccountSetup />
</>
// </PrivateRoute>
}
/>
<Route
path="/basket"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<Basket />
</>
}
/>
<Route
path="/paymenthistory"
element={
// <PrivateRoute>
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<PaymentHistory />
</>
// </PrivateRoute>
}
/>
</Routes> </Routes>
</> </>
); );

42
src/model/auth.ts Normal file

@ -0,0 +1,42 @@
export interface RegisterRequest {
login: string;
email: string;
password: string;
phoneNumber: string;
};
export interface RegisterResponse {
accessToken: string;
login: string;
email: string;
phoneNumber: string;
refreshToken: string;
_id: string;
}
export interface LoginRequest {
email: string;
password: string;
}
export type LoginResponse = RegisterResponse;
export interface CreateUserRequest {
login: string;
email: string;
password: string;
phoneNumber: string;
};
export interface User {
_id: string;
login: string;
email: string;
phoneNumber: string;
isDeleted: boolean;
createdAt: string;
updatedAt: string;
deletedAt?: string;
}

@ -1,10 +1,13 @@
import { Box } from "@mui/material"; import { Box, CssBaseline, ThemeProvider } from "@mui/material";
import Section1 from "./Section1"; import Section1 from "./Section1";
import Section2 from "./Section2"; import Section2 from "./Section2";
import Section3 from "./Section3"; import Section3 from "./Section3";
import Section4 from "./Section4"; import Section4 from "./Section4";
import Section5 from "./Section5"; import Section5 from "./Section5";
import FloatingSupportChat from "@root/components/FloatingSupportChat/FloatingSupportChat"; import FloatingSupportChat from "@root/components/FloatingSupportChat/FloatingSupportChat";
import Footer from "@root/components/Footer";
import darkTheme from "@root/utils/themes/dark";
import Navbar from "@root/components/Navbar/Navbar";
interface Props { interface Props {
templaterOnly?: boolean; templaterOnly?: boolean;
@ -13,15 +16,20 @@ interface Props {
export default function Landing({ templaterOnly = false }: Props) { export default function Landing({ templaterOnly = false }: Props) {
return ( return (
<Box sx={{ <ThemeProvider theme={darkTheme}>
position: "relative", <CssBaseline />
}}> <Box sx={{
<Section1 /> position: "relative",
<Section2 templaterOnly={templaterOnly}/> }}>
<Section3 /> <Navbar isLoggedIn={false} />
<Section4 /> <Section1 />
<Section5 /> <Section2 templaterOnly={templaterOnly} />
<FloatingSupportChat /> <Section3 />
</Box> <Section4 />
<Section5 />
<Footer />
<FloatingSupportChat />
</Box>
</ThemeProvider>
); );
} }

@ -6,10 +6,12 @@ import { authStore } from "@stores/makeRequest";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import InputTextfield from "@components/InputTextfield"; import InputTextfield from "@components/InputTextfield";
import PenaLogo from "@components/PenaLogo"; import PenaLogo from "@components/PenaLogo";
import { useSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { object, string } from "yup"; import { object, string } from "yup";
import { useState } from "react"; import { useState } from "react";
import { LoginRequest, LoginResponse } from "@root/model/auth";
import { setUserId, setUserEmail, setUserPassword } from "@root/stores/user";
interface Values { interface Values {
@ -27,10 +29,8 @@ const validationSchema = object({
password: string().required("Поле обязательно"), password: string().required("Поле обязательно"),
}); });
// TODO remove useSnackbar anywhere
export default function SigninDialog() { export default function SigninDialog() {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();
@ -39,18 +39,25 @@ export default function SigninDialog() {
const formik = useFormik<Values>({ const formik = useFormik<Values>({
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values: Values) => { onSubmit: (values, formikHelpers) => {
makeRequest({ makeRequest<LoginRequest, LoginResponse>({
url: "https://hub.pena.digital/auth/login", url: "https://hub.pena.digital/auth/login",
body: { body: {
"email": values.email, email: values.email,
"password": values.password password: values.password
}, },
useToken: false useToken: false,
}) withCredentials: true,
.catch((e: any) => { }).then(result => {
enqueueSnackbar(e.message ? e.message : `Unknown error`); setUserId(result._id);
}); setUserEmail(result.email);
setUserPassword(values.password);
navigate("/tariffs");
}).catch((error: any) => {
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Unknown error");
}).finally(() => {
formikHelpers.setSubmitting(false);
});
}, },
}); });

@ -5,11 +5,13 @@ import CloseIcon from "@mui/icons-material/Close";
import CustomButton from "@components/CustomButton"; import CustomButton from "@components/CustomButton";
import InputTextfield from "@components/InputTextfield"; import InputTextfield from "@components/InputTextfield";
import PenaLogo from "@components/PenaLogo"; import PenaLogo from "@components/PenaLogo";
import { useSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { authStore } from "@stores/makeRequest"; import { authStore } from "@stores/makeRequest";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { object, ref, string } from "yup"; import { object, ref, string } from "yup";
import { ChangeEvent, useState } from "react"; import { ChangeEvent, useState } from "react";
import { RegisterRequest, RegisterResponse } from "@root/model/auth";
import { setUserEmail, setUserPassword, setUserId } from "@root/stores/user";
interface Values { interface Values {
@ -28,14 +30,13 @@ const initialValues: Values = {
const validationSchema = object({ const validationSchema = object({
email: string().email("Введите email").required("Поле обязательно"), email: string().email("Введите email").required("Поле обязательно"),
phoneNumber: string().matches(/^\+\d{5,}|\d{6,}$/, "Введите номер телефона").required("Поле обязательно"), phoneNumber: string().min(6, "Введите номер телефона").matches(/^\+\d+|\d+$/, "Введите номер телефона").required("Поле обязательно"),
password: string().min(8, "Минимум 8 символов").required("Поле обязательно"), password: string().min(8, "Минимум 8 символов").matches(/^[.,:;-_+\d\w]+$/, "Некорректные символы").required("Поле обязательно"),
repeatPassword: string().oneOf([ref("password"), undefined], "Пароли не совпадают"), repeatPassword: string().oneOf([ref("password"), undefined], "Пароли не совпадают"),
}); });
export default function SignupDialog() { export default function SignupDialog() {
const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true); const [isDialogOpen, setIsDialogOpen] = useState<boolean>(true);
const { enqueueSnackbar } = useSnackbar();
const theme = useTheme(); const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")); const upMd = useMediaQuery(theme.breakpoints.up("md"));
const navigate = useNavigate(); const navigate = useNavigate();
@ -44,21 +45,27 @@ export default function SignupDialog() {
const formik = useFormik<Values>({ const formik = useFormik<Values>({
initialValues, initialValues,
validationSchema, validationSchema,
onSubmit: async (values: Values) => { onSubmit: (values, formikHelpers) => {
makeRequest({ makeRequest<RegisterRequest, RegisterResponse>({
url: "https://hub.pena.digital/auth/register", url: "https://hub.pena.digital/auth/register",
body: { body: {
"login": values.email, login: values.email,
"email": values.email, email: values.email,
"password": values.repeatPassword, password: values.repeatPassword,
"phoneNumber": "--" phoneNumber: values.phoneNumber,
}, },
useToken: false useToken: false,
}) withCredentials: true,
.catch((e: any) => { }).then(result => {
console.log(e); setUserId(result._id);
enqueueSnackbar(e.response?.data?.message ?? `Unknown error`); setUserEmail(result.email);
}); setUserPassword(values.password);
navigate("/tariffs");
}).catch((error: any) => {
enqueueSnackbar(error.response?.data?.message ?? error.message ?? "Unknown error");
}).finally(() => {
formikHelpers.setSubmitting(false);
});
}, },
}); });

43
src/stores/user.ts Normal file

@ -0,0 +1,43 @@
import { User } from "@root/model/auth";
import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware";
interface UserStore {
userId: string | null;
email: string | null;
password: string | null;
user: User | null;
}
const initialState: UserStore = {
userId: null,
email: null,
password: null,
user: null,
};
export const useUserStore = create<UserStore>()(
persist(
devtools(
(set, get) => initialState,
{
name: "User store",
}
),
{
name: "user",
storage: createJSONStorage(() => localStorage),
partialize: state => ({
userId: state.userId,
}),
}
)
);
export const setUserId = (userId: string | null) => useUserStore.setState({ userId });
export const setUserEmail = (email: string | null) => useUserStore.setState({ email });
export const setUserPassword = (password: string | null) => useUserStore.setState({ password });
export const setUser = (user: User | null) => useUserStore.setState({ user });
export const clearUser = () => useUserStore.setState({ ...initialState });

@ -1,15 +1,9 @@
import * as React from "react"; import { Navigate, Outlet } from 'react-router-dom';
import { useLocation, Navigate } from 'react-router-dom' import { useUserStore } from '@root/stores/user';
import {authStore} from "@stores/makeRequest";
export default ({ children }: any) => {
const location = useLocation()
const { token } = authStore()
console.log(token)
//Если пользователь авторизован, перенаправляем его на нужный путь. Иначе выкидываем в регистрацию
if (token) {
return children
}
return <Navigate to="/signin" state={{from: location}} />
} export default function PrivateRoute() {
const user = useUserStore(state => state.user);
return user ? <Outlet /> : <Navigate to="/" replace />;
}