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 NavbarFull from "./NavbarFull";
interface Props {
isLoggedIn: boolean;
isCollapsed?: boolean;
isCollapsed?: boolean;
isLoggedIn: boolean;
}
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 MenuIcon from "@mui/icons-material/Menu";
import PenaLogo from "../PenaLogo";
interface Props {
isLoggedIn: boolean;
isLoggedIn: boolean;
}
export default function NavbarCollapsed({ isLoggedIn }: Props) {
const theme = useTheme();
const theme = useTheme();
return (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.navbarbg.main,
position: "sticky",
top: 0,
zIndex: 1,
// borderBottom: "1px solid #E3E3E3",
}}
sx={{
height: "51px",
py: "6px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<PenaLogo width={100} />
<IconButton sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}>
<MenuIcon sx={{ height: "30px", width: "30px" }} />
</IconButton>
</SectionWrapper>
);
return (
<>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.navbarbg.main,
position: "sticky",
top: 0,
zIndex: 1,
// borderBottom: "1px solid #E3E3E3",
}}
sx={{
height: "51px",
py: "6px",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<PenaLogo width={100} />
<IconButton sx={{ p: 0, width: "30px", color: theme.palette.primary.main }}>
<MenuIcon sx={{ height: "30px", width: "30px" }} />
</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 { 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 { basketStore } from "@stores/BasketStore";
@ -12,6 +12,9 @@ import CustomAvatar from "./Avatar";
import Drawers from "../Drawers";
import PenaLogo from "../PenaLogo";
import Menu from "../Menu";
import { logout } from "@root/api/auth";
import { enqueueSnackbar } from "notistack";
import { clearUser, useUserStore } from "@root/stores/user";
interface Props {
isLoggedIn: boolean;
@ -21,6 +24,8 @@ export default function NavbarFull({ isLoggedIn }: Props) {
const theme = useTheme();
const { clearToken } = authStore();
const location = useLocation();
const navigate = useNavigate();
const user = useUserStore(state => state.user);
const { open } = basketStore();
@ -31,7 +36,15 @@ export default function NavbarFull({ isLoggedIn }: Props) {
}, [location.pathname, open]);
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 ? (
@ -85,40 +98,43 @@ export default function NavbarFull({ isLoggedIn }: Props) {
</Box>
</Container>
) : (
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
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"
<>
<SectionWrapper
component="nav"
maxWidth="lg"
outerContainerSx={{
backgroundColor: theme.palette.lightPurple.main,
// borderBottom: "1px solid #E3E3E3",
}}
sx={{
px: "18px",
py: "10px",
borderColor: "white",
borderRadius: "8px",
whiteSpace: "nowrap",
minWidth: "180px",
px: "20px",
display: "flex",
justifyContent: "space-between",
height: "80px",
alignItems: "center",
gap: "50px",
}}
>
Личный кабинет
</Button>
</SectionWrapper>
<PenaLogo width={150} />
<Menu />
<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 { 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 Wallet from "./pages/Wallet";
import Payment from "./pages/Payment/Payment";
@ -15,23 +15,34 @@ import SignupDialog from "./pages/auth/Signup";
import PaymentHistory from "./pages/PaymentHistory/PaymentHistory";
import Basket from "./pages/Basket/Basket";
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 PublicRoute from "@utils/routes/publicRoute";
import PrivateRoute from "@utils/routes/privateRoute";
import reportWebVitals from "./reportWebVitals";
import { SnackbarProvider } from "notistack";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
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 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 (
<>
@ -42,133 +53,23 @@ const App = () => {
</Routes>
}
<Routes location={state?.backgroundLocation || location}>
<Route
path="/"
element={
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Navbar isLoggedIn={false} isCollapsed={!upMd} />
<Divider sx={{ bgcolor: "#E3E3E3", borderColor: "#00000000" }} />
<Landing />
<Footer />
</ThemeProvider>
}
/>
<Route
path="tariffs"
element={
<>
<Navbar isLoggedIn={true} isCollapsed={!upMd} />
<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>
}
/>
<Route path="/" element={<Landing />} />
<Route element={<Layout />}>
<Route path="/tariffs" element={<Tariffs />} />
<Route path="/tariffs/time" element={<TariffPage />} />
<Route path="/tariffs/volume" element={<TariffPage />} />
<Route path="/faq" element={<Faq />} />
<Route path="/support" element={<Support />} />
<Route path="/support/:ticketId" element={<Support />} />
<Route path="/tariffconstructor" element={<CustomTariff />} />
<Route path="/basket" element={<Basket />} />
<Route element={<PrivateRoute />}>
<Route path="/wallet" element={<Wallet />} />
<Route path="/payment" element={<Payment />} />
<Route path="/settings" element={<AccountSetup />} />
<Route path="/paymenthistory" element={<PaymentHistory />} />
</Route>
</Route>
</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 Section2 from "./Section2";
import Section3 from "./Section3";
import Section4 from "./Section4";
import Section5 from "./Section5";
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 {
templaterOnly?: boolean;
@ -13,15 +16,20 @@ interface Props {
export default function Landing({ templaterOnly = false }: Props) {
return (
<Box sx={{
position: "relative",
}}>
<Section1 />
<Section2 templaterOnly={templaterOnly}/>
<Section3 />
<Section4 />
<Section5 />
<FloatingSupportChat />
</Box>
<ThemeProvider theme={darkTheme}>
<CssBaseline />
<Box sx={{
position: "relative",
}}>
<Navbar isLoggedIn={false} />
<Section1 />
<Section2 templaterOnly={templaterOnly} />
<Section3 />
<Section4 />
<Section5 />
<Footer />
<FloatingSupportChat />
</Box>
</ThemeProvider>
);
}

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

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 { useLocation, Navigate } from 'react-router-dom'
import {authStore} from "@stores/makeRequest";
import { Navigate, Outlet } from 'react-router-dom';
import { useUserStore } from '@root/stores/user';
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 />;
}