diff --git a/package.json b/package.json index 869c895..4d696ff 100644 --- a/package.json +++ b/package.json @@ -74,6 +74,8 @@ "@loadable/server": "^5.15.0", "@reduxjs/toolkit": "^1.6.0", "@sendgrid/mail": "^7.6.0", + "@types/cookie-parser": "^1.4.2", + "@types/js-cookie": "^3.0.1", "@types/lodash": "^4.14.177", "autoprefixer": "^10.2.6", "axios": "^0.21.1", @@ -81,6 +83,7 @@ "chalk": "^4.1.1", "compression": "^1.7.4", "connected-react-router": "^6.9.1", + "cookie-parser": "^1.4.6", "core-js": "^3.15.2", "cross-env": "^7.0.3", "express": "^4.17.1", @@ -93,6 +96,7 @@ "hpp": "^0.2.3", "html-minifier": "^4.0.0", "https": "^1.0.0", + "js-cookie": "^3.0.1", "jsonwebtoken": "^8.5.1", "lodash": "^4.17.21", "morgan": "^1.10.0", diff --git a/src/client/index.tsx b/src/client/index.tsx index 342ffdc..2960f9b 100755 --- a/src/client/index.tsx +++ b/src/client/index.tsx @@ -3,13 +3,19 @@ import { Provider } from "react-redux" import { ConnectedRouter } from "connected-react-router" import { RouteConfig, renderRoutes } from "react-router-config" import { loadableReady } from "@loadable/component" +import Cookies from "js-cookie" import createStore from "../store" import routes from "../routes" +const storage: any = localStorage + // Get the initial state from server-side rendering const initialState = window.__INITIAL_STATE__ -const { store, history } = createStore({ initialState }) + +const id = +(Cookies.get("id") || storage?.getItem("id")) +const jwt = Cookies.get("jwt") || storage?.getItem("jwt") +const { store, history } = createStore({ initialState, id, jwt }) const render = (Routes: RouteConfig[]) => ReactDOM.hydrate( diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/index.tsx similarity index 95% rename from src/components/LoginForm/LoginForm.tsx rename to src/components/LoginForm/index.tsx index 0b21ea5..806de2c 100644 --- a/src/components/LoginForm/LoginForm.tsx +++ b/src/components/LoginForm/index.tsx @@ -28,7 +28,7 @@ const LoginForm = ({ dispatch, error }: Props): JSX.Element => { return (
- Connectez-vous pour accéder à votre espace. + Si vous êtes bénévole, connectez-vous pour accéder à votre espace.
diff --git a/src/components/Notifications/index.tsx b/src/components/Notifications/index.tsx new file mode 100644 index 0000000..b917387 --- /dev/null +++ b/src/components/Notifications/index.tsx @@ -0,0 +1,162 @@ +import _ from "lodash" +import React, { memo, useCallback, useState } from "react" +import { AppDispatch } from "../../store" +import { fetchVolunteerNotifsSet } from "../../store/volunteerNotifsSet" +import { VolunteerNotifs } from "../../services/volunteers" +import styles from "./styles.module.scss" +import { logoutUser } from "../../store/auth" +import { unsetJWT } from "../../services/auth" + +interface Props { + dispatch: AppDispatch + jwt: string + // eslint-disable-next-line react/require-default-props + volunteerNotifs?: VolunteerNotifs +} + +const Notifications = ({ dispatch, jwt, volunteerNotifs }: Props): JSX.Element => { + const hidden = volunteerNotifs?.hiddenNotifs || [] + const notifs: JSX.Element[] = [] + + const onSubmit1 = useCallback( + (event: React.SyntheticEvent): void => { + event.preventDefault() + dispatch( + fetchVolunteerNotifsSet(jwt, 0, { + hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 1], + }) + ) + }, + [dispatch, jwt, volunteerNotifs] + ) + + if (!_.includes(hidden, 1)) { + notifs.push( +
+
+
+ + Salut {volunteerNotifs?.firstname} ! +
+ Ici tu seras notifié(e) des nouvelles importantes et des questions + pour lesquelles il nous faudrait absolument ta réponse. +
+ +
+
+ +
+
+
+ ) + } + + const [participation, setParticipation] = useState(volunteerNotifs?.active || "inconnu") + const onChangeValue2 = (e: React.ChangeEvent) => + setParticipation(e.target.value) + + const onSubmit2 = useCallback( + (event: React.SyntheticEvent): void => { + event.preventDefault() + + dispatch( + fetchVolunteerNotifsSet(jwt, 0, { + hiddenNotifs: [...(volunteerNotifs?.hiddenNotifs || []), 2], + active: participation, + }) + ) + }, + [dispatch, jwt, volunteerNotifs, participation] + ) + + if (!_.includes(hidden, 2)) { + notifs.push( +
+
+
+
+
+ Si les conditions sanitaires te le permettent, souhaites-tu être + bénévole à PeL 2022 ?
+ {" "} + -
+ {" "} + Oui +
+ {" "} + Non +
+ {" "} + Je ne sais pas encore +
+ {participation === "peut-etre" ? ( +
+ On te redemandera dans quelques temps. Si tu as des + questions +
+ ) : null} +
+ +
+
+
+
+
+
+ ) + } + + /* DISCORD + Discord nous donne à tous la parole via nos téléphone ou navigateurs, pour organiser le meilleur des festivals ! +Il permet de discuter sujet par sujet entre tous les bénévoles, entre les membres d'une même équipe, ou avec ton référent. +Il permet de choisir les sujets spécifiques sur lesquels être notifié de nouveaux messages. + +Rejoindre les 86 bénévoles déjà présents sur le serveur se fait en cliquant ici. +Tu n'y es absolument pas obligé(e) ! C'est juste plus pratique. +*/ + + const onClick = useCallback( + (event: React.SyntheticEvent): void => { + event.preventDefault() + unsetJWT() + dispatch(logoutUser()) + }, + [dispatch] + ) + + notifs.push( +
+ +
+ ) + + return
{notifs.map((t) => t).reduce((prev, curr) => [prev, curr])}
+} + +export default memo(Notifications) diff --git a/src/components/Notifications/styles.module.scss b/src/components/Notifications/styles.module.scss new file mode 100755 index 0000000..463c191 --- /dev/null +++ b/src/components/Notifications/styles.module.scss @@ -0,0 +1,41 @@ +@import "../../theme/variables"; +@import "../../theme/mixins"; + +.notificationsPage { + @include page-wrapper-center; +} + +.notificationsContent { + @include page-content-wrapper; +} + +.notifIntro { + margin-bottom: 10px; +} + +.formLine { + padding: 5px 0; + + label { + display: block; + margin-left: 5px; + } + select, + input { + width: 10%; + border: 1px solid #333; + border-radius: 4px; + } +} + +.formButtons { + margin-top: 10px; + padding: 5px 0; + text-align: center; +} + +.error { + margin-top: 10px; + color: rgb(255, 0, 0); + text-align: center; +} diff --git a/src/components/RegisterForm/index.tsx b/src/components/PreRegisterForm/index.tsx similarity index 98% rename from src/components/RegisterForm/index.tsx rename to src/components/PreRegisterForm/index.tsx index 53dbc0a..39b79cd 100644 --- a/src/components/RegisterForm/index.tsx +++ b/src/components/PreRegisterForm/index.tsx @@ -12,7 +12,7 @@ interface Props { preVolunteerCount: number | undefined } -const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { +const PreRegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { const [firstname, setFirstname] = useState("") const [lastname, setLastname] = useState("") const [email, setEmail] = useState("") @@ -97,7 +97,7 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { return (
-
+
Qu'est-ce que Paris est Ludique ?

@@ -253,4 +253,4 @@ const RegisterForm = ({ dispatch, preVolunteerCount }: Props): JSX.Element => { ) } -export default memo(RegisterForm) +export default memo(PreRegisterForm) diff --git a/src/components/RegisterForm/styles.module.scss b/src/components/PreRegisterForm/styles.module.scss similarity index 98% rename from src/components/RegisterForm/styles.module.scss rename to src/components/PreRegisterForm/styles.module.scss index 41736ad..e6e854c 100755 --- a/src/components/RegisterForm/styles.module.scss +++ b/src/components/PreRegisterForm/styles.module.scss @@ -1,7 +1,7 @@ @import "../../theme/variables"; @import "../../theme/mixins"; -.registerIntro { +.preRegisterIntro { dt { font-weight: bold; margin-top: 10px; diff --git a/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx b/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx index 787d2dc..7f361c8 100755 --- a/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx +++ b/src/components/VolunteerInfo/__tests__/VolunteerInfo.tsx @@ -3,6 +3,7 @@ */ import { render } from "@testing-library/react" import { MemoryRouter } from "react-router-dom" +import { volunteerExample } from "../../../services/volunteers" import VolunteerInfo from "../index" @@ -10,23 +11,7 @@ describe("", () => { it("renders", () => { const tree = render( - + ).container.firstChild diff --git a/src/components/VolunteerList/__tests__/VolunteerList.tsx b/src/components/VolunteerList/__tests__/VolunteerList.tsx index 4f05e07..e075bff 100755 --- a/src/components/VolunteerList/__tests__/VolunteerList.tsx +++ b/src/components/VolunteerList/__tests__/VolunteerList.tsx @@ -4,33 +4,14 @@ import { render } from "@testing-library/react" import { MemoryRouter } from "react-router-dom" +import { volunteerExample } from "../../../services/volunteers" import VolunteerList from "../index" describe("", () => { it("renders", () => { const tree = render( - + ).container.firstChild diff --git a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx b/src/components/VolunteerSet/__tests__/VolunteerSet.tsx index 8d107bc..45ff57d 100644 --- a/src/components/VolunteerSet/__tests__/VolunteerSet.tsx +++ b/src/components/VolunteerSet/__tests__/VolunteerSet.tsx @@ -3,6 +3,7 @@ */ import { render } from "@testing-library/react" import { MemoryRouter } from "react-router-dom" +import { volunteerExample } from "../../../services/volunteers" import VolunteerSet from "../index" @@ -11,24 +12,7 @@ describe("", () => { const dispatch = jest.fn() const tree = render( - + ).container.firstChild diff --git a/src/components/index.ts b/src/components/index.ts index 114c892..e38540b 100755 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,5 @@ +import LoginForm from "./LoginForm" +import Notifications from "./Notifications" import VolunteerList from "./VolunteerList" import JavGameList from "./JavGameList" import VolunteerInfo from "./VolunteerInfo" @@ -5,9 +7,11 @@ import VolunteerSet from "./VolunteerSet" import ErrorBoundary from "./ErrorBoundary" import Loading from "./Loading" import WishAdd from "./WishAdd" -import RegisterForm from "./RegisterForm" +import PreRegisterForm from "./PreRegisterForm" export { + LoginForm, + Notifications, VolunteerList, JavGameList, VolunteerInfo, @@ -15,5 +19,5 @@ export { ErrorBoundary, Loading, WishAdd, - RegisterForm, + PreRegisterForm, } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx new file mode 100644 index 0000000..7a9583a --- /dev/null +++ b/src/pages/Home/Home.tsx @@ -0,0 +1,50 @@ +import { FC, memo } from "react" +import { RouteComponentProps, Link } from "react-router-dom" +import { useDispatch, useSelector, shallowEqual } from "react-redux" +import { Helmet } from "react-helmet" + +import { AppState, AppThunk } from "../../store" +import { LoginForm, Notifications } from "../../components" +import styles from "./styles.module.scss" +import { fetchVolunteerNotifsSetIfNeed } from "../../store/volunteerNotifsSet" + +export type Props = RouteComponentProps + +const HomePage: FC = (): JSX.Element => { + const dispatch = useDispatch() + const readyStatus = useSelector((state: AppState) => state.volunteerNotifsSet.readyStatus) + const volunteerNotifs = useSelector( + (state: AppState) => state.volunteerNotifsSet.entity, + shallowEqual + ) + + const loginError = useSelector((state: AppState) => state.volunteerLogin.error, shallowEqual) + const jwt = useSelector((state: AppState) => state.auth.jwt, shallowEqual) + + if (!readyStatus || readyStatus === "idle" || readyStatus === "request") + return

Loading...

+ + if (jwt) { + return + } + return ( +
+
+
+ + +
+
+
+
+ S'informer sur le bénévolat +
+
+
+ ) +} + +// Fetch server-side data here +export const loadData = (): AppThunk[] => [fetchVolunteerNotifsSetIfNeed()] + +export default memo(HomePage) diff --git a/src/pages/Home/HomePage.tsx b/src/pages/Home/HomePage.tsx deleted file mode 100755 index 9e95112..0000000 --- a/src/pages/Home/HomePage.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { FC, memo } from "react" -import { RouteComponentProps } from "react-router-dom" -import { Helmet } from "react-helmet" - -import { AppThunk } from "../../store" -import styles from "./styles.module.scss" - -export type Props = RouteComponentProps - -const fetchUserData = () => () => Promise.resolve() - -const HomePage: FC = (): JSX.Element => ( -
-
- -
Tableau de bord
-
-
-) - -// Fetch server-side data here -export const loadData = (): AppThunk[] => [ - fetchUserData(), - // More pre-fetched actions... -] - -export default memo(HomePage) diff --git a/src/pages/Home/index.tsx b/src/pages/Home/index.tsx index 0e32223..f1cf6b7 100755 --- a/src/pages/Home/index.tsx +++ b/src/pages/Home/index.tsx @@ -1,15 +1,16 @@ import loadable from "@loadable/component" import { Loading, ErrorBoundary } from "../../components" -import { Props, loadData } from "./HomePage" +import { Props, loadData } from "./Home" -const Home = loadable(() => import("./HomePage"), { +const HomePage = loadable(() => import("./Home"), { fallback: , }) export default (props: Props): JSX.Element => ( - + ) + export { loadData } diff --git a/src/pages/Home/styles.module.scss b/src/pages/Home/styles.module.scss index 1423793..0cab755 100755 --- a/src/pages/Home/styles.module.scss +++ b/src/pages/Home/styles.module.scss @@ -4,6 +4,14 @@ @include page-wrapper-center; } -.homeContent { - @include page-content-wrapper(600px); +.notificationsContent { + @include page-content-wrapper; +} + +.loginContent { + @include page-content-wrapper; +} + +.preRegisterContent { + @include page-content-wrapper; } diff --git a/src/pages/Login/LoginPage.tsx b/src/pages/Login/LoginPage.tsx index 316bb0b..9f0dac0 100644 --- a/src/pages/Login/LoginPage.tsx +++ b/src/pages/Login/LoginPage.tsx @@ -4,7 +4,7 @@ import React, { memo } from "react" import { Helmet } from "react-helmet" import { AppState } from "../../store" -import LoginForm from "../../components/LoginForm/LoginForm" +import { LoginForm } from "../../components" import styles from "./styles.module.scss" export type Props = RouteComponentProps diff --git a/src/pages/Register/RegisterPage.tsx b/src/pages/PreRegister/PreRegister.tsx similarity index 77% rename from src/pages/Register/RegisterPage.tsx rename to src/pages/PreRegister/PreRegister.tsx index f2ffd66..a262c5d 100644 --- a/src/pages/Register/RegisterPage.tsx +++ b/src/pages/PreRegister/PreRegister.tsx @@ -5,7 +5,7 @@ import { Helmet } from "react-helmet" import { AppState, AppThunk, ValueRequest } from "../../store" import { fetchPreVolunteerCountIfNeed } from "../../store/preVolunteerCount" -import { RegisterForm } from "../../components" +import { PreRegisterForm } from "../../components" import styles from "./styles.module.scss" export type Props = RouteComponentProps @@ -29,14 +29,14 @@ function useList( if (readyStatus === "failure") return

Oops, Failed to load!

- return + return } } -const RegisterPage: FC = (): JSX.Element => ( -
-
- +const PreRegisterPage: FC = (): JSX.Element => ( +
+
+ {useList((state: AppState) => state.preVolunteerCount, fetchPreVolunteerCountIfNeed)()}
@@ -45,4 +45,4 @@ const RegisterPage: FC = (): JSX.Element => ( // Fetch server-side data here export const loadData = (): AppThunk[] => [fetchPreVolunteerCountIfNeed()] -export default memo(RegisterPage) +export default memo(PreRegisterPage) diff --git a/src/pages/Register/index.tsx b/src/pages/PreRegister/index.tsx similarity index 57% rename from src/pages/Register/index.tsx rename to src/pages/PreRegister/index.tsx index a8f3dc2..0c8f838 100755 --- a/src/pages/Register/index.tsx +++ b/src/pages/PreRegister/index.tsx @@ -1,14 +1,16 @@ import loadable from "@loadable/component" import { Loading, ErrorBoundary } from "../../components" -import { Props } from "./RegisterPage" +import { Props, loadData } from "./PreRegister" -const RegisterPage = loadable(() => import("./RegisterPage"), { +const PreRegister = loadable(() => import("./PreRegister"), { fallback: , }) export default (props: Props): JSX.Element => ( - + ) + +export { loadData } diff --git a/src/pages/Register/styles.module.scss b/src/pages/PreRegister/styles.module.scss similarity index 73% rename from src/pages/Register/styles.module.scss rename to src/pages/PreRegister/styles.module.scss index c7f3efb..7d678c1 100755 --- a/src/pages/Register/styles.module.scss +++ b/src/pages/PreRegister/styles.module.scss @@ -1,9 +1,9 @@ @import "../../theme/mixins"; -.registerPage { +.preRegisterPage { @include page-wrapper-center; } -.registerContent { +.preRegisterContent { @include page-content-wrapper(600px); } diff --git a/src/routes/index.ts b/src/routes/index.ts index 2cd6af8..11082a4 100755 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -2,11 +2,11 @@ import { RouteConfig } from "react-router-config" import App from "../app" import AsyncHome, { loadData as loadHomeData } from "../pages/Home" +import AsyncPreRegisterPage, { loadData as loadPreRegisterPage } from "../pages/PreRegister" import AsyncWish, { loadData as loadWishData } from "../pages/Wish" import AsyncVolunteerPage, { loadData as loadVolunteerPageData } from "../pages/VolunteerPage" import Login from "../pages/Login" import Forgot from "../pages/Forgot" -import Register from "../pages/Register" import NotFound from "../pages/NotFound" export default [ @@ -16,7 +16,13 @@ export default [ { path: "/", exact: true, - component: Register, + component: AsyncHome, + loadData: loadHomeData, + }, + { + path: "/preRegister", + component: AsyncPreRegisterPage, + loadData: loadPreRegisterPage, }, { path: "/VolunteerPage/:id", @@ -31,11 +37,6 @@ export default [ path: "/forgot", component: Forgot, }, - { - path: "/home", - component: AsyncHome, - loadData: loadHomeData, - }, { path: "/wish", component: AsyncWish, diff --git a/src/server/gsheets/expressAccessors.ts b/src/server/gsheets/expressAccessors.ts index 8d53844..e03a394 100644 --- a/src/server/gsheets/expressAccessors.ts +++ b/src/server/gsheets/expressAccessors.ts @@ -37,7 +37,7 @@ export default class ExpressAccessors< } // custom can be async - get(custom?: (list: Element[], body: Request["body"]) => Promise | any) { + get(custom?: (list: Element[], body: Request["body"], id: number) => Promise | any) { return async (request: Request, response: Response, _next: NextFunction): Promise => { try { const list = (await this.sheet.getList()) || [] @@ -46,7 +46,12 @@ export default class ExpressAccessors< const id = parseInt(request.query.id as string, 10) || -1 toCaller = list.find((e: Element) => e.id === id) } else { - toCaller = await custom(list, request.body) + const memberId = response?.locals?.jwt?.id || -1 + toCaller = await custom(list, request.body, memberId) + if (toCaller?.jwt && toCaller?.id) { + response.cookie("jwt", toCaller.jwt, { maxAge: 365 * 24 * 60 * 60 }) + response.cookie("id", toCaller.id, { maxAge: 365 * 24 * 60 * 60 }) + } } response.status(200).json(toCaller) } catch (e: any) { @@ -72,7 +77,8 @@ export default class ExpressAccessors< set( custom?: ( list: Element[], - body: RequestBody + body: RequestBody, + id: number ) => Promise> | CustomSetReturn ) { return async (request: Request, response: Response, _next: NextFunction): Promise => { @@ -81,8 +87,9 @@ export default class ExpressAccessors< await this.sheet.set(request.body) response.status(200) } else { + const memberId = response?.locals?.jwt?.id || -1 const list = (await this.sheet.getList()) || [] - const { toDatabase, toCaller } = await custom(list, request.body) + const { toDatabase, toCaller } = await custom(list, request.body, memberId) if (toDatabase !== undefined) { await this.sheet.set(toDatabase) } diff --git a/src/server/gsheets/volunteers.ts b/src/server/gsheets/volunteers.ts index 83c45e7..f767f09 100644 --- a/src/server/gsheets/volunteers.ts +++ b/src/server/gsheets/volunteers.ts @@ -3,7 +3,13 @@ import bcrypt from "bcrypt" import sgMail from "@sendgrid/mail" import ExpressAccessors, { RequestBody } from "./expressAccessors" -import { Volunteer, VolunteerWithoutId, translationVolunteer } from "../../services/volunteers" +import { + Volunteer, + VolunteerWithoutId, + VolunteerLogin, + VolunteerNotifs, + translationVolunteer, +} from "../../services/volunteers" import { canonicalEmail } from "../../utils/standardization" import { getJwt } from "../secure" @@ -14,38 +20,39 @@ const expressAccessor = new ExpressAccessors( ) export const volunteerListGet = expressAccessor.listGet() -export const volunteerGet = expressAccessor.get() export const volunteerAdd = expressAccessor.add() export const volunteerSet = expressAccessor.set() -export const volunteerLogin = expressAccessor.get(async (list: Volunteer[], body: RequestBody) => { - const volunteer = getByEmail(list, body.email) - if (!volunteer) { - throw Error("Il n'y a aucun bénévole avec cet email") - } +export const volunteerLogin = expressAccessor.get( + async (list: Volunteer[], body: RequestBody): Promise => { + const volunteer = getByEmail(list, body.email) + if (!volunteer) { + throw Error("Il n'y a aucun bénévole avec cet email") + } - const password = body.password || "" - const password1Match = await bcrypt.compare( - password, - volunteer.password1.replace(/^\$2y/, "$2a") - ) - if (!password1Match) { - const password2Match = await bcrypt.compare( + const password = body.password || "" + const password1Match = await bcrypt.compare( password, - volunteer.password2.replace(/^\$2y/, "$2a") + volunteer.password1.replace(/^\$2y/, "$2a") ) - if (!password2Match) { - throw Error("Mauvais mot de passe pour cet email") + if (!password1Match) { + const password2Match = await bcrypt.compare( + password, + volunteer.password2.replace(/^\$2y/, "$2a") + ) + if (!password2Match) { + throw Error("Mauvais mot de passe pour cet email") + } + } + + const jwt = await getJwt(volunteer.id) + + return { + id: volunteer.id, + jwt, } } - - const jwt = await getJwt(volunteer.email) - - return { - firstname: volunteer.firstname, - jwt, - } -}) +) const lastForgot: { [id: string]: number } = {} export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], body: RequestBody) => { @@ -78,6 +85,29 @@ export const volunteerForgot = expressAccessor.set(async (list: Volunteer[], bod } }) +export const volunteerNotifsSet = expressAccessor.set( + async (list: Volunteer[], body: RequestBody, id: number) => { + const volunteer = list.find((v) => v.id === id) + if (!volunteer) { + throw Error(`Il n'y a aucun bénévole avec cet identifiant ${id}`) + } + const newVolunteer = _.cloneDeep(volunteer) + + _.assign(newVolunteer, _.pick(body[1], _.keys(newVolunteer))) + + return { + toDatabase: newVolunteer, + toCaller: { + id: newVolunteer.id, + firstname: newVolunteer.firstname, + adult: newVolunteer.adult, + active: newVolunteer.active, + hiddenNotifs: newVolunteer.hiddenNotifs, + } as VolunteerNotifs, + } + } +) + function getByEmail(list: Volunteer[], rawEmail: string): Volunteer | undefined { const email = canonicalEmail(rawEmail || "") const volunteer = list.find((v) => canonicalEmail(v.email) === email) diff --git a/src/server/index.ts b/src/server/index.ts index bf22730..c2522d5 100755 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,6 +1,7 @@ import path from "path" import express, { RequestHandler } from "express" import logger from "morgan" +import cookieParser from "cookie-parser" import compression from "compression" import helmet from "helmet" import hpp from "hpp" @@ -19,7 +20,12 @@ import { secure } from "./secure" import { javGameListGet } from "./gsheets/javGames" import { wishListGet, wishAdd } from "./gsheets/wishes" import { preVolunteerAdd, preVolunteerCountGet } from "./gsheets/preVolunteers" -import { volunteerGet, volunteerSet, volunteerLogin, volunteerForgot } from "./gsheets/volunteers" +import { + volunteerNotifsSet, + volunteerSet, + volunteerLogin, + volunteerForgot, +} from "./gsheets/volunteers" import config from "../config" const app = express() @@ -45,6 +51,7 @@ app.use(express.static(path.resolve(process.cwd(), "public"))) if (__DEV__) devServer(app) app.use(express.json()) +app.use(cookieParser()) /** * APIs @@ -59,8 +66,9 @@ app.post("/VolunteerLogin", volunteerLogin) app.post("/VolunteerForgot", volunteerForgot) // Secured APIs -app.get("/VolunteerGet", secure as RequestHandler, volunteerGet) app.post("/VolunteerSet", secure as RequestHandler, volunteerSet) +// UNSAFE app.post("/VolunteerGet", secure as RequestHandler, volunteerGet) +app.post("/VolunteerNotifsSet", secure as RequestHandler, volunteerNotifsSet) // Use React server-side rendering middleware app.get("*", ssr) diff --git a/src/server/secure.ts b/src/server/secure.ts index e0481fe..04210c5 100644 --- a/src/server/secure.ts +++ b/src/server/secure.ts @@ -2,7 +2,6 @@ import { NextFunction, Request, Response } from "express" import path from "path" import { promises as fs } from "fs" import { verify, sign } from "jsonwebtoken" -import { canonicalEmail } from "../utils/standardization" import config from "../config" @@ -20,16 +19,17 @@ export function secure(request: AuthorizedRequest, response: Response, next: Nex } const rawToken = request.headers.authorization - const token = rawToken && rawToken.split(/\s/)[1] + const token1 = rawToken && rawToken.split(/\s/)[1] + const token2 = request.cookies?.jwt + const token = token1 || token2 verify(token, cachedSecret, (tokenError: any, decoded: any) => { if (tokenError) { - response.status(403).json({ - error: "Invalid token, please Log in first, criterion auth", + response.status(200).json({ + error: "Accès interdit sans identification", }) return } - decoded.user.replace(/@gmailcom$/, "@gmail.com") response.locals.jwt = decoded next() }) @@ -50,9 +50,9 @@ async function getSecret() { return cachedSecret } -export async function getJwt(email: string): Promise { +export async function getJwt(id: number): Promise { const jwt = sign( - { user: canonicalEmail(email), permissions: [] }, + { id }, await getSecret() // __TEST__ // ? undefined diff --git a/src/server/ssr.tsx b/src/server/ssr.tsx index 7ddbdac..fbc87a9 100644 --- a/src/server/ssr.tsx +++ b/src/server/ssr.tsx @@ -12,9 +12,11 @@ import { Action } from "@reduxjs/toolkit" import createStore from "../store" import renderHtml from "./renderHtml" import routes from "../routes" +import { getCookieJWT } from "../services/auth" export default async (req: Request, res: Response, next: NextFunction): Promise => { - const { store } = createStore({ url: req.url }) + const { jwt, id } = getCookieJWT(req.headers.cookie) + const { store } = createStore({ url: req.url, jwt, id }) // The method for loading data from server-side const loadBranchData = (): Promise => { diff --git a/src/services/accessors.ts b/src/services/accessors.ts index 75af8e8..d84b151 100644 --- a/src/services/accessors.ts +++ b/src/services/accessors.ts @@ -1,4 +1,5 @@ import axios from "axios" +import _ from "lodash" import config from "../config" import { axiosConfig } from "./auth" @@ -123,7 +124,9 @@ export default class ServiceAccessors< } } - customPost(apiName: string): (params: any) => Promise<{ + customPost>( + apiName: string + ): (...params: InputElements) => Promise<{ data?: any error?: Error }> { @@ -131,7 +134,7 @@ export default class ServiceAccessors< data?: any error?: Error } - return async (params: any): Promise => { + return async (...params: InputElements): Promise => { try { const { data } = await axios.post( `${config.API_URL}/${this.elementName}${apiName}`, @@ -147,4 +150,36 @@ export default class ServiceAccessors< } } } + + securedCustomPost>( + apiName: string + ): ( + jwt: string, + ...params: InputElements + ) => Promise<{ + data?: any + error?: Error + }> { + interface ElementGetResponse { + data?: any + error?: Error + } + return async (jwt: string, ...params: InputElements): Promise => { + try { + const auth = { headers: { Authorization: `Bearer ${jwt}` } } + const fullAxiosConfig = _.defaultsDeep(auth, axiosConfig) + const { data } = await axios.post( + `${config.API_URL}/${this.elementName}${apiName}`, + params, + fullAxiosConfig + ) + if (data.error) { + throw Error(data.error) + } + return { data } + } catch (error) { + return { error: error as Error } + } + } + } } diff --git a/src/services/auth.ts b/src/services/auth.ts index 5fb7332..bb06259 100644 --- a/src/services/auth.ts +++ b/src/services/auth.ts @@ -1,4 +1,7 @@ import { AxiosRequestConfig } from "axios" +import Cookies from "js-cookie" + +import { VolunteerLogin } from "./volunteers" const storage: any = localStorage @@ -6,17 +9,30 @@ export const axiosConfig: AxiosRequestConfig = { headers: {}, } -const jwt: string | null = storage?.getItem("id_token") -if (jwt) { - setJWT(jwt) -} - -export function setJWT(token: string): void { +export function setJWT(token: string, id: number): void { axiosConfig.headers.Authorization = `Bearer ${token}` - storage?.setItem("id_token", token) + storage?.setItem("jwt", token) + storage?.setItem("id", id) + Cookies.set("jwt", token) + Cookies.set("id", `${id}`) } export function unsetJWT(): void { delete axiosConfig.headers.Authorization - storage?.removeItem("id_token") + storage?.removeItem("jwt") + storage?.removeItem("id") + + Cookies.remove("jwt") + Cookies.remove("id") +} + +export function getCookieJWT(cookie = ""): VolunteerLogin { + const cookies = cookie + .split(";") + .reduce((res: { [cookieName: string]: string }, el: string) => { + const [k, v] = el.split("=") + res[k.trim()] = v + return res + }, {}) + return { jwt: cookies.jwt, id: +cookies.id } } diff --git a/src/services/volunteers.ts b/src/services/volunteers.ts index f0f9153..76592cf 100644 --- a/src/services/volunteers.ts +++ b/src/services/volunteers.ts @@ -19,7 +19,9 @@ export class Volunteer { privileges = 0 - active = 0 + active = "" + + hiddenNotifs: number[] = [] created = new Date() @@ -39,6 +41,7 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = { adult: "majeur", privileges: "privilege", active: "actif", + hiddenNotifs: "notifsCachees", created: "creation", password1: "passe1", password2: "passe2", @@ -46,6 +49,23 @@ export const translationVolunteer: { [k in keyof Volunteer]: string } = { const elementName = "Volunteer" +export const volunteerExample: Volunteer = { + id: 1, + firstname: "Aupeix", + lastname: "Amélie", + email: "pakouille.lakouille@yahoo.fr", + mobile: "0675650392", + photo: "images/volunteers/$taille/amélie_aupeix.jpg", + food: "Végétarien", + adult: 1, + privileges: 0, + active: "inconnu", + hiddenNotifs: [], + created: new Date(0), + password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", + password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPkdq9d5fqpbl8ASimSjNj4SR.9O", +} + export const emailRegexp = /^(([^<>()[\].,;:\s@"]+(\.[^<>()[\].,;:\s@"]+)*)|(".+"))@(([^<>()[\].,;:\s@"]+\.)+[^<>()[\].,;:\s@"]{2,})$/i export const passwordMinLength = 4 @@ -60,12 +80,23 @@ export const volunteerAdd = serviceAccessors.add() export const volunteerSet = serviceAccessors.set() export interface VolunteerLogin { - firstname: string + id: number jwt: string } -export const volunteerLogin = serviceAccessors.customPost("Login") +export const volunteerLogin = + serviceAccessors.customPost<[{ email: string; password: string }]>("Login") export interface VolunteerForgot { message: string } -export const volunteerForgot = serviceAccessors.customPost("Forgot") +export const volunteerForgot = serviceAccessors.customPost<[{ email: string }]>("Forgot") + +export interface VolunteerNotifs { + id: number + firstname: string + adult: number + active: string + hiddenNotifs: number[] +} +export const volunteerNotifsSet = + serviceAccessors.securedCustomPost<[number, Partial]>("NotifsSet") diff --git a/src/store/__tests__/volunteer.ts b/src/store/__tests__/volunteer.ts index 9492fcc..4a5f993 100644 --- a/src/store/__tests__/volunteer.ts +++ b/src/store/__tests__/volunteer.ts @@ -8,25 +8,11 @@ import volunteer, { fetchVolunteer, initialState, } from "../volunteer" -import { Volunteer } from "../../services/volunteers" +import { Volunteer, volunteerExample } from "../../services/volunteers" jest.mock("axios") -const mockData: Volunteer = { - id: 1, - lastname: "Aupeix", - firstname: "Amélie", - email: "pakouille.lakouille@yahoo.fr", - mobile: "0675650392", - photo: "images/volunteers/$taille/amélie_aupeix.jpg", - food: "Végétarien", - adult: 1, - privileges: 0, - active: 0, - created: new Date(0), - password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", - password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", -} +const mockData: Volunteer = volunteerExample const { id } = mockData const mockError = "Oops! Something went wrong." diff --git a/src/store/__tests__/volunteerList.ts b/src/store/__tests__/volunteerList.ts index 9d1a3fd..fa069f7 100644 --- a/src/store/__tests__/volunteerList.ts +++ b/src/store/__tests__/volunteerList.ts @@ -9,27 +9,11 @@ import volunteerList, { getFailure, fetchVolunteerList, } from "../volunteerList" -import { Volunteer } from "../../services/volunteers" +import { Volunteer, volunteerExample } from "../../services/volunteers" jest.mock("axios") -const mockData: Volunteer[] = [ - { - id: 1, - lastname: "Aupeix", - firstname: "Amélie", - email: "pakouille.lakouille@yahoo.fr", - mobile: "0675650392", - photo: "images/volunteers/$taille/amélie_aupeix.jpg", - food: "Végétarien", - adult: 1, - privileges: 0, - active: 0, - created: new Date(0), - password1: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", - password2: "$2y$10$fSxY9AIuxSiEjwF.J3eXGubIxUPlobkyRrNIal8ASimSjNj4SR.9O", - }, -] +const mockData: Volunteer[] = [volunteerExample] const mockError = "Oops! Something went wrong." describe("volunteerList reducer", () => { diff --git a/src/store/auth.ts b/src/store/auth.ts new file mode 100644 index 0000000..4dc468c --- /dev/null +++ b/src/store/auth.ts @@ -0,0 +1,35 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit" +import { AppState } from "." + +// Define a type for the slice state +interface AuthState { + id: number + jwt: string +} + +// Define the initial state using that type +const initialState: AuthState = { + id: 0, + jwt: "", +} + +export const auth = createSlice({ + name: "auth", + initialState, + reducers: { + setCurrentUser: (state, action: PayloadAction) => { + state.id = action.payload.id + state.jwt = action.payload.jwt + }, + logoutUser: (state) => { + state.id = 0 + state.jwt = "" + }, + }, +}) + +export const { setCurrentUser, logoutUser } = auth.actions + +export const selectCount = (state: AppState): AuthState => state.auth + +export default auth.reducer diff --git a/src/store/index.ts b/src/store/index.ts index 191b432..af49c9f 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -2,18 +2,22 @@ import { createMemoryHistory, createBrowserHistory } from "history" import { Action, configureStore, EntityState } from "@reduxjs/toolkit" import { ThunkAction } from "redux-thunk" import { routerMiddleware } from "connected-react-router" +import Cookies from "js-cookie" import createRootReducer from "./rootReducer" import { StateRequest } from "./utils" +import { setCurrentUser, logoutUser } from "./auth" interface Arg { initialState?: typeof window.__INITIAL_STATE__ url?: string + jwt?: string + id?: number } // Use inferred return type for making correctly Redux types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const createStore = ({ initialState, url }: Arg = {}) => { +const createStore = ({ initialState, url, jwt, id }: Arg = {}) => { const history = __SERVER__ ? createMemoryHistory({ initialEntries: [url || "/"] }) : createBrowserHistory() @@ -28,10 +32,20 @@ const createStore = ({ initialState, url }: Arg = {}) => { devTools: __DEV__, }) + if (jwt && id) { + store.dispatch(setCurrentUser({ jwt, id })) + } else { + store.dispatch(logoutUser()) + } + return { store, history } } -const { store } = createStore() +const storage: any = localStorage +const id = +(Cookies.get("id") || storage?.getItem("id")) +const jwt = Cookies.get("jwt") || storage?.getItem("jwt") + +const { store } = createStore({ id, jwt }) export type AppState = ReturnType diff --git a/src/store/rootReducer.ts b/src/store/rootReducer.ts index 587927b..90f0a81 100644 --- a/src/store/rootReducer.ts +++ b/src/store/rootReducer.ts @@ -1,6 +1,7 @@ import { History } from "history" import { connectRouter } from "connected-react-router" +import auth from "./auth" import wishAdd from "./wishAdd" import wishList from "./wishList" import javGameList from "./javGameList" @@ -10,12 +11,14 @@ import volunteerList from "./volunteerList" import volunteerSet from "./volunteerSet" import volunteerLogin from "./volunteerLogin" import volunteerForgot from "./volunteerForgot" +import volunteerNotifsSet from "./volunteerNotifsSet" import preVolunteerAdd from "./preVolunteerAdd" import preVolunteerCount from "./preVolunteerCount" // Use inferred return type for making correctly Redux types // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export default (history: History) => ({ + auth, wishAdd, wishList, javGameList, @@ -25,6 +28,7 @@ export default (history: History) => ({ volunteerSet, volunteerLogin, volunteerForgot, + volunteerNotifsSet, preVolunteerAdd, preVolunteerCount, router: connectRouter(history) as any, diff --git a/src/store/utils.ts b/src/store/utils.ts index 44659ad..9ee4032 100644 --- a/src/store/utils.ts +++ b/src/store/utils.ts @@ -1,7 +1,7 @@ import { ActionCreatorWithoutPayload, ActionCreatorWithPayload } from "@reduxjs/toolkit" import { toast } from "react-toastify" -import { AppThunk } from "." +import { AppThunk, AppDispatch } from "." export interface StateRequest { readyStatus: "idle" | "request" | "success" | "failure" @@ -32,8 +32,8 @@ export function toastSuccess(message: string): void { }) } -export function elementFetch( - elementService: (...idArgs: any[]) => Promise<{ +export function elementFetch>( + elementService: (...idArgs: ServiceInput) => Promise<{ data?: Element | undefined error?: Error | undefined }>, @@ -41,9 +41,9 @@ export function elementFetch( getSuccess: ActionCreatorWithPayload, getFailure: ActionCreatorWithPayload, errorMessage?: (error: Error) => void, - successMessage?: (data: Element) => void -): (...idArgs: any[]) => AppThunk { - return (...idArgs: any[]): AppThunk => + successMessage?: (data: Element, dispatch: AppDispatch) => void +): (...idArgs: ServiceInput) => AppThunk { + return (...idArgs: ServiceInput): AppThunk => async (dispatch) => { dispatch(getRequesting()) @@ -54,7 +54,7 @@ export function elementFetch( errorMessage?.(error) } else { dispatch(getSuccess(data as Element)) - successMessage?.(data as Element) + successMessage?.(data as Element, dispatch) } } } diff --git a/src/store/volunteerLogin.ts b/src/store/volunteerLogin.ts index 5b727e0..bc7a2c8 100644 --- a/src/store/volunteerLogin.ts +++ b/src/store/volunteerLogin.ts @@ -3,6 +3,9 @@ import { PayloadAction, createSlice } from "@reduxjs/toolkit" import { StateRequest, elementFetch } from "./utils" import { VolunteerLogin, volunteerLogin } from "../services/volunteers" import { setJWT } from "../services/auth" +import { AppDispatch } from "." +import { setCurrentUser } from "./auth" +import { fetchVolunteerNotifsSet } from "./volunteerNotifsSet" type StateVolunteer = { entity?: VolunteerLogin } & StateRequest @@ -31,13 +34,15 @@ const volunteerLoginSlice = createSlice({ export default volunteerLoginSlice.reducer export const { getRequesting, getSuccess, getFailure } = volunteerLoginSlice.actions -export const fetchVolunteerLogin = elementFetch( +export const fetchVolunteerLogin = elementFetch>( volunteerLogin, getRequesting, getSuccess, getFailure, undefined, - (login: VolunteerLogin) => { - setJWT(login.jwt) + (login: VolunteerLogin, dispatch: AppDispatch) => { + setJWT(login.jwt, login.id) + dispatch(setCurrentUser(login)) + dispatch(fetchVolunteerNotifsSet(login.jwt, login.id, {})) } ) diff --git a/src/store/volunteerNotifsSet.ts b/src/store/volunteerNotifsSet.ts new file mode 100644 index 0000000..09ea239 --- /dev/null +++ b/src/store/volunteerNotifsSet.ts @@ -0,0 +1,57 @@ +import { PayloadAction, createSlice } from "@reduxjs/toolkit" + +import { StateRequest, toastError, elementFetch } from "./utils" +import { VolunteerNotifs, volunteerNotifsSet } from "../services/volunteers" +import { AppThunk, AppState } from "." + +type StateVolunteerNotifsSet = { entity?: VolunteerNotifs } & StateRequest + +export const initialState: StateVolunteerNotifsSet = { + readyStatus: "idle", +} + +const volunteerNotifsSetSlice = createSlice({ + name: "volunteerNotifsSet", + initialState, + reducers: { + getRequesting: (_) => ({ + readyStatus: "request", + }), + getSuccess: (_, { payload }: PayloadAction) => ({ + readyStatus: "success", + entity: payload, + }), + getFailure: (_, { payload }: PayloadAction) => ({ + readyStatus: "failure", + error: payload, + }), + }, +}) + +export default volunteerNotifsSetSlice.reducer +export const { getRequesting, getSuccess, getFailure } = volunteerNotifsSetSlice.actions + +export const fetchVolunteerNotifsSet = elementFetch( + volunteerNotifsSet, + getRequesting, + getSuccess, + getFailure, + (error: Error) => toastError(`Erreur lors du chargement des notifications: ${error.message}`) +) + +const shouldFetchVolunteerNotifsSet = (state: AppState, id: number) => + state.volunteerNotifsSet.readyStatus !== "success" || + (state.volunteerNotifsSet.entity && state.volunteerNotifsSet.entity.id !== id) + +export const fetchVolunteerNotifsSetIfNeed = + (id = 0, notif: Partial = {}): AppThunk => + (dispatch, getState) => { + let jwt = "" + if (!id) { + ;({ id, jwt } = getState().auth) + } + if (shouldFetchVolunteerNotifsSet(getState(), id)) + return dispatch(fetchVolunteerNotifsSet(jwt, id, notif)) + + return null + } diff --git a/yarn.lock b/yarn.lock index f28276d..09f3529 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1566,6 +1566,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.yarnpkg.com/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/css-minimizer-webpack-plugin@^3.0.2": version "3.0.2" resolved "https://registry.yarnpkg.com/@types/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-3.0.2.tgz#ed58bbbde8a7b7591118aa93d8f8d0cdc0cc6173" @@ -1725,6 +1732,11 @@ jest-diff "^26.0.0" pretty-format "^26.0.0" +"@types/js-cookie@^3.0.1": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.1.tgz#04aa743e2e0a85a22ee9aa61f6591a8bc19b5d68" + integrity sha512-7wg/8gfHltklehP+oyJnZrz9XBuX5ZPP4zB6UsI84utdlkRYLnOm2HfpLXazTwZA+fpGn0ir8tGNgVnMEleBGQ== + "@types/json-schema@*", "@types/json-schema@^7.0.4", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.7", "@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": version "7.0.9" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" @@ -3500,6 +3512,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.6: + version "1.4.6" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.6.tgz#3ac3a7d35a7a03bbc7e365073a26074824214594" + integrity sha512-z3IzaNjdwUC2olLIB5/ITd0/setiaFMLYiZJle7xg5Fe9KWAceil7xszYfHHBtDFYLSgJduS2Ty0P1uJdPDJeA== + dependencies: + cookie "0.4.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -3510,6 +3530,11 @@ cookie@0.4.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.0.tgz#beb437e7022b3b6d49019d088665303ebe9c14ba" integrity sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== +cookie@0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1" + integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA== + core-js-compat@^3.18.0, core-js-compat@^3.19.1: version "3.19.1" resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.19.1.tgz#fe598f1a9bf37310d77c3813968e9f7c7bb99476" @@ -6790,6 +6815,11 @@ js-base64@^2.1.8: resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-2.6.4.tgz#f4e686c5de1ea1f867dbcad3d46d969428df98c4" integrity sha512-pZe//GGmwJndub7ZghVHz7vjb2LgC1m8B07Au3eYqeqv9emhESByMXxaEgkUkEqJe87oBbSniGYoQNIBklc7IQ== +js-cookie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.1.tgz#9e39b4c6c2f56563708d7d31f6f5f21873a92414" + integrity sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"