浏览代码

feat: implement google auth

AykutSarac 2 年之前
父节点
当前提交
21717be7f3

+ 3 - 0
package.json

@@ -14,11 +14,14 @@
   },
   "dependencies": {
     "@monaco-editor/react": "^4.4.6",
+    "@react-oauth/google": "^0.4.0",
     "@sentry/nextjs": "^7.16.0",
     "allotment": "^1.17.0",
+    "axios": "^1.1.3",
     "compress-json": "^2.1.2",
     "html-to-image": "^1.10.8",
     "jsonc-parser": "^3.2.0",
+    "jwt-decode": "^3.1.2",
     "next": "^12.3.1",
     "next-transpile-modules": "^9.1.0",
     "react": "^18.2.0",

+ 4 - 2
src/components/Modal/index.tsx

@@ -17,7 +17,9 @@ type ModalTypes = {
 
 export interface ModalProps {
   visible: boolean;
-  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
+  setVisible:
+    | React.Dispatch<React.SetStateAction<boolean>>
+    | ((visible: boolean) => void);
   size?: "md" | "lg";
 }
 
@@ -56,7 +58,7 @@ const Modal: React.FC<React.PropsWithChildren<ModalProps>> & ModalTypes = ({
 }) => {
   const onClick = (e: React.SyntheticEvent<HTMLDivElement>) => {
     if (e.currentTarget === e.target) {
-      setVisible(v => !v);
+      setVisible(false);
     }
   };
 

+ 26 - 0
src/components/ModalController/index.tsx

@@ -0,0 +1,26 @@
+import React from "react";
+import { ClearModal } from "src/containers/Modals/ClearModal";
+import { CloudModal } from "src/containers/Modals/CloudModal";
+import { DownloadModal } from "src/containers/Modals/DownloadModal";
+import { ImportModal } from "src/containers/Modals/ImportModal";
+import { LoginModal } from "src/containers/Modals/LoginModal";
+import { SettingsModal } from "src/containers/Modals/SettingsModal";
+import { ShareModal } from "src/containers/Modals/ShareModal";
+import useModal from "src/store/useModal";
+
+export const ModalController = () => {
+  const setVisible = useModal(state => state.setVisible);
+  const state = useModal(state => state);
+
+  return (
+    <>
+      <ImportModal visible={state.import} setVisible={setVisible("import")} />
+      <ClearModal visible={state.clear} setVisible={setVisible("clear")} />
+      <DownloadModal visible={state.download} setVisible={setVisible("download")} />
+      <SettingsModal visible={state.settings} setVisible={setVisible("settings")} />
+      <CloudModal visible={state.cloud} setVisible={setVisible("cloud")} />
+      <LoginModal visible={state.login} setVisible={setVisible("login")} />
+      <ShareModal visible={state.share} setVisible={setVisible("share")} />
+    </>
+  );
+};

+ 8 - 22
src/components/Sidebar/index.tsx

@@ -19,13 +19,9 @@ import {
   VscSettingsGear,
 } from "react-icons/vsc";
 import { Tooltip } from "src/components/Tooltip";
-import { ClearModal } from "src/containers/Modals/ClearModal";
-import { CloudModal } from "src/containers/Modals/CloudModal";
-import { DownloadModal } from "src/containers/Modals/DownloadModal";
-import { ImportModal } from "src/containers/Modals/ImportModal";
-import { SettingsModal } from "src/containers/Modals/SettingsModal";
 import useConfig from "src/store/useConfig";
 import useGraph from "src/store/useGraph";
+import useModal from "src/store/useModal";
 import { getNextDirection } from "src/utils/getNextDirection";
 import styled from "styled-components";
 import shallow from "zustand/shallow";
@@ -144,12 +140,7 @@ function rotateLayout(direction: "LEFT" | "RIGHT" | "DOWN" | "UP") {
 }
 
 export const Sidebar: React.FC = () => {
-  const [cloudmodalVisible, setCloudmodalVisible] = React.useState(false);
-  const [settingsVisible, setSettingsVisible] = React.useState(false);
-  const [uploadVisible, setUploadVisible] = React.useState(false);
-  const [clearVisible, setClearVisible] = React.useState(false);
-  const [isDownloadVisible, setDownloadVisible] = React.useState(false);
-
+  const setVisible = useModal(state => state.setVisible);
   const getJson = useConfig(state => state.getJson);
   const setDirection = useGraph(state => state.setDirection);
   const setConfig = useConfig(state => state.setConfig);
@@ -210,7 +201,7 @@ export const Sidebar: React.FC = () => {
         </Tooltip>
 
         <Tooltip title="Import File">
-          <StyledElement onClick={() => setUploadVisible(true)}>
+          <StyledElement onClick={() => setVisible("import")(true)}>
             <AiOutlineFileAdd />
           </StyledElement>
         </Tooltip>
@@ -252,40 +243,35 @@ export const Sidebar: React.FC = () => {
         </Tooltip>
 
         <Tooltip className="mobile" title="Download Image">
-          <StyledElement onClick={() => setDownloadVisible(true)}>
+          <StyledElement onClick={() => setVisible("download")(true)}>
             <FiDownload />
           </StyledElement>
         </Tooltip>
 
         <Tooltip title="Clear JSON">
-          <StyledElement onClick={() => setClearVisible(true)}>
+          <StyledElement onClick={() => setVisible("clear")(true)}>
             <AiOutlineDelete />
           </StyledElement>
         </Tooltip>
 
         <Tooltip className="desktop" title="View Saved JSON">
-          <StyledElement onClick={() => setCloudmodalVisible(true)}>
+          <StyledElement onClick={() => setVisible("cloud")(true)}>
             <VscCloud />
           </StyledElement>
         </Tooltip>
       </StyledTopWrapper>
       <StyledBottomWrapper>
         <Tooltip title="Account">
-          <StyledElement>
+          <StyledElement onClick={() => setVisible("login")(true)}>
             <VscAccount />
           </StyledElement>
         </Tooltip>
         <Tooltip title="Setings">
-          <StyledElement onClick={() => setSettingsVisible(true)}>
+          <StyledElement onClick={() => setVisible("settings")(true)}>
             <VscSettingsGear />
           </StyledElement>
         </Tooltip>
       </StyledBottomWrapper>
-      <ImportModal visible={uploadVisible} setVisible={setUploadVisible} />
-      <ClearModal visible={clearVisible} setVisible={setClearVisible} />
-      <DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
-      <SettingsModal visible={settingsVisible} setVisible={setSettingsVisible} />
-      <CloudModal visible={cloudmodalVisible} setVisible={setCloudmodalVisible} />
     </StyledSidebar>
   );
 };

+ 6 - 5
src/containers/Editor/BottomBar.tsx

@@ -5,8 +5,9 @@ import {
   AiOutlineUnlock,
 } from "react-icons/ai";
 import { VscAccount, VscHeart } from "react-icons/vsc";
+import useModal from "src/store/useModal";
+import useUser from "src/store/useUser";
 import styled from "styled-components";
-import { ShareModal } from "../Modals/ShareModal";
 
 const StyledBottomBar = styled.div`
   display: flex;
@@ -51,14 +52,15 @@ const StyledBottomBarItem = styled.button`
 `;
 
 export const BottomBar = () => {
-  const [shareVisible, setShareVisible] = React.useState(false);
+  const user = useUser(state => state.user);
+  const setVisible = useModal(state => state.setVisible);
 
   return (
     <StyledBottomBar>
       <StyledLeft>
         <StyledBottomBarItem>
           <VscAccount />
-          Aykut Saraç
+          {user ? user.name : "Login"}
         </StyledBottomBarItem>
         <StyledBottomBarItem>
           <AiOutlineCloudUpload />
@@ -68,7 +70,7 @@ export const BottomBar = () => {
           <AiOutlineUnlock />
           Public
         </StyledBottomBarItem>
-        <StyledBottomBarItem onClick={() => setShareVisible(true)}>
+        <StyledBottomBarItem onClick={() => setVisible("share")(true)}>
           <AiOutlineLink />
           Share
         </StyledBottomBarItem>
@@ -79,7 +81,6 @@ export const BottomBar = () => {
           Support JSON Crack
         </StyledBottomBarItem>
       </StyledRight>
-      <ShareModal visible={shareVisible} setVisible={setShareVisible} />
     </StyledBottomBar>
   );
 };

+ 3 - 4
src/containers/Editor/Tools.tsx

@@ -4,8 +4,8 @@ import { FiDownload } from "react-icons/fi";
 import { MdCenterFocusWeak } from "react-icons/md";
 import { SearchInput } from "src/components/SearchInput";
 import useConfig from "src/store/useConfig";
+import useModal from "src/store/useModal";
 import styled from "styled-components";
-import { DownloadModal } from "../Modals/DownloadModal";
 
 export const StyledTools = styled.div`
   position: relative;
@@ -46,7 +46,7 @@ const StyledToolElement = styled.button`
 `;
 
 export const Tools: React.FC = () => {
-  const [isDownloadVisible, setDownloadVisible] = React.useState(false);
+  const setVisible = useModal(state => state.setVisible);
 
   const hideEditor = useConfig(state => state.hideEditor);
   const setConfig = useConfig(state => state.setConfig);
@@ -65,7 +65,7 @@ export const Tools: React.FC = () => {
         <SearchInput />
         <StyledToolElement
           aria-label="save"
-          onClick={() => setDownloadVisible(true)}
+          onClick={() => setVisible("download")(true)}
         >
           <FiDownload />
         </StyledToolElement>
@@ -79,7 +79,6 @@ export const Tools: React.FC = () => {
           <AiOutlinePlus />
         </StyledToolElement>
       </StyledTools>
-      <DownloadModal visible={isDownloadVisible} setVisible={setDownloadVisible} />
     </>
   );
 };

+ 74 - 0
src/containers/Modals/LoginModal/index.tsx

@@ -0,0 +1,74 @@
+import React from "react";
+import { GoogleLogin } from "@react-oauth/google";
+import toast from "react-hot-toast";
+import { Modal, ModalProps } from "src/components/Modal";
+import useUser from "src/store/useUser";
+import styled from "styled-components";
+
+const StyledTitle = styled.p`
+  display: flex;
+  align-items: center;
+  color: ${({ theme }) => theme.TEXT_POSITIVE};
+  flex: 1;
+  font-weight: 700;
+  font-family: "Catamaran", sans-serif;
+  margin-top: 0;
+
+  &::after {
+    background: ${({ theme }) => theme.TEXT_POSITIVE};
+    height: 1px;
+
+    content: "";
+    -webkit-box-flex: 1;
+    -ms-flex: 1 1 auto;
+    flex: 1 1 auto;
+    margin-left: 4px;
+    opacity: 0.6;
+  }
+`;
+
+const StyledModalContent = styled.div`
+  margin-bottom: 20px;
+`;
+
+const StyledLoginWrapper = styled.div`
+  display: flex;
+  justify-content: center;
+  align-items: center;
+`;
+
+export const LoginModal: React.FC<ModalProps> = ({ setVisible, visible }) => {
+  const login = useUser(state => state.login);
+
+  return (
+    <Modal visible={visible} setVisible={setVisible}>
+      <Modal.Header>Login to JSON Crack</Modal.Header>
+      <Modal.Content>
+        <StyledTitle>Join Now!</StyledTitle>
+        <StyledModalContent>
+          Login to JSON Crack to{" "}
+          <b>
+            save your JSON files, share links and directly embed into your websites
+          </b>{" "}
+          without JavaScript instantly!
+        </StyledModalContent>
+        <StyledLoginWrapper>
+          <GoogleLogin
+            auto_select
+            width="210"
+            onSuccess={credentialResponse => {
+              if (credentialResponse.credential) {
+                login(credentialResponse.credential);
+                setVisible(false);
+              }
+            }}
+            onError={() => {
+              toast.error("Unable to login to Google account!");
+            }}
+          />
+        </StyledLoginWrapper>
+      </Modal.Content>
+      <Modal.Controls setVisible={setVisible} />
+    </Modal>
+  );
+};

+ 2 - 5
src/containers/Modals/SettingsModal/index.tsx

@@ -1,5 +1,5 @@
 import React from "react";
-import { Modal } from "src/components/Modal";
+import { Modal, ModalProps } from "src/components/Modal";
 import Toggle from "src/components/Toggle";
 import useStored from "src/store/useStored";
 import styled from "styled-components";
@@ -16,10 +16,7 @@ const StyledModalWrapper = styled.div`
   gap: 20px;
 `;
 
-export const SettingsModal: React.FC<{
-  visible: boolean;
-  setVisible: React.Dispatch<React.SetStateAction<boolean>>;
-}> = ({ visible, setVisible }) => {
+export const SettingsModal: React.FC<ModalProps> = ({ visible, setVisible }) => {
   const lightmode = useStored(state => state.lightmode);
   const setLightTheme = useStored(state => state.setLightTheme);
   const [toggleHideCollapse, hideCollapse] = useStored(

+ 11 - 3
src/pages/_app.tsx

@@ -1,14 +1,17 @@
 import React from "react";
 import type { AppProps } from "next/app";
 import { useRouter } from "next/router";
+import { GoogleOAuthProvider } from "@react-oauth/google";
 import { init } from "@sentry/nextjs";
 import { decompress } from "compress-json";
 import { Toaster } from "react-hot-toast";
 import { GoogleAnalytics } from "src/components/GoogleAnalytics";
+import { ModalController } from "src/components/ModalController";
 import GlobalStyle from "src/constants/globalStyle";
 import { darkTheme, lightTheme } from "src/constants/theme";
 import useConfig from "src/store/useConfig";
 import useStored from "src/store/useStored";
+import useUser from "src/store/useUser";
 import { isValidJson } from "src/utils/isValidJson";
 import { ThemeProvider } from "styled-components";
 
@@ -24,6 +27,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
   const lightmode = useStored(state => state.lightmode);
   const setJson = useConfig(state => state.setJson);
   const [isRendered, setRendered] = React.useState(false);
+  const checkSession = useUser(state => state.checkSession);
 
   React.useEffect(() => {
     try {
@@ -48,7 +52,10 @@ function JsonCrack({ Component, pageProps }: AppProps) {
 
   if (isRendered)
     return (
-      <>
+      <GoogleOAuthProvider
+        onScriptLoadSuccess={() => checkSession()}
+        clientId="34440253867-g7s6qhtaqe7lumj1vutctm49t8dhvcf9.apps.googleusercontent.com"
+      >
         <GoogleAnalytics />
         <ThemeProvider theme={lightmode ? lightTheme : darkTheme}>
           <GlobalStyle />
@@ -58,7 +65,7 @@ function JsonCrack({ Component, pageProps }: AppProps) {
             containerStyle={{
               top: 40,
               right: 6,
-              fontSize: 14
+              fontSize: 14,
             }}
             toastOptions={{
               style: {
@@ -67,8 +74,9 @@ function JsonCrack({ Component, pageProps }: AppProps) {
               },
             }}
           />
+          <ModalController />
         </ThemeProvider>
-      </>
+      </GoogleOAuthProvider>
     );
 }
 

+ 9 - 0
src/services/google.ts

@@ -0,0 +1,9 @@
+import axios, { AxiosResponse } from "axios";
+
+const validateToken = async <T>(token: string): Promise<AxiosResponse<T>> => {
+  return await axios.get(
+    `https://oauth2.googleapis.com/tokeninfo?id_token=${token}`
+  );
+};
+
+export { validateToken };

+ 40 - 0
src/store/useModal.tsx

@@ -0,0 +1,40 @@
+// TODO: handle all modals here
+// put all modals into a global place
+// display auth modal for unauthorized modal access actions
+import create from "zustand";
+import useUser from "./useUser";
+
+interface ModalActions {
+  setVisible: (modal: keyof typeof initialStates) => (visible: boolean) => void;
+}
+
+const initialStates = {
+  clear: false,
+  cloud: false,
+  download: false,
+  goals: false,
+  import: false,
+  login: false,
+  node: false,
+  settings: false,
+  share: false,
+};
+
+type ModalType = keyof typeof initialStates;
+
+const authModals: ModalType[] = ["cloud", "share"];
+
+export type ModalStates = typeof initialStates;
+
+const useModal = create<ModalStates & ModalActions>()(set => ({
+  ...initialStates,
+  setVisible: modal => visible => {
+    if (authModals.includes(modal) && !useUser.getState().isAuthorized) {
+      return set({ login: true });
+    }
+
+    set({ [modal]: visible });
+  },
+}));
+
+export default useModal;

+ 57 - 0
src/store/useUser.tsx

@@ -0,0 +1,57 @@
+import jwt_decode from "jwt-decode";
+import { validateToken } from "src/services/google";
+import create from "zustand";
+
+interface GoogleUser {
+  iss: string;
+  nbf: number;
+  aud: string;
+  sub: string;
+  email: string;
+  email_verified: boolean;
+  azp: string;
+  name: string;
+  picture: string;
+  given_name: string;
+  family_name: string;
+  iat: number;
+  exp: number;
+  jti: string;
+}
+
+interface UserActions {
+  login: (credential: string) => void;
+  setUser: (key: keyof typeof initialStates, value: any) => void;
+  checkSession: () => void;
+}
+
+const initialStates = {
+  isAuthorized: false,
+  user: null as GoogleUser | null,
+};
+
+export type UserStates = typeof initialStates;
+
+const useUser = create<UserStates & UserActions>()(set => ({
+  ...initialStates,
+  setUser: (key, value) => set({ [key]: value }),
+  checkSession: async () => {
+    const token = localStorage.getItem("auth_token");
+
+    if (token) {
+      try {
+        const { data: user } = await validateToken<GoogleUser>(token);
+        set({ user, isAuthorized: true });
+      } catch (error) {
+        set({ isAuthorized: false });
+      }
+    }
+  },
+  login: credential => {
+    const googleUser = jwt_decode<GoogleUser>(credential);
+    localStorage.setItem("auth_token", credential);
+    set({ user: googleUser, isAuthorized: true });
+  },
+}));
+
+export default useUser;

+ 62 - 0
yarn.lock

@@ -1291,6 +1291,11 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
+"@react-oauth/google@^0.4.0":
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/@react-oauth/google/-/google-0.4.0.tgz#ae4fe2724040bd11facdc53aad43a21e9f34b2c9"
+  integrity sha512-2QxxrKbXXH8bwHSefB56sBgsKs7Bq3Pvv8tVmGJuINGefECsssIUKidTDm5P55T4CV99sCX/GUfxs3l2Ntxo8Q==
+
 "@rollup/plugin-babel@^5.2.0":
   version "5.3.1"
   resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283"
@@ -1887,6 +1892,11 @@ async@^3.2.3:
   resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
   integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
 
+asynckit@^0.4.0:
+  version "0.4.0"
+  resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+  integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
 at-least-node@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2"
@@ -1897,6 +1907,15 @@ axe-core@^4.4.3:
   resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.3.tgz#11c74d23d5013c0fa5d183796729bc3482bd2f6f"
   integrity sha512-32+ub6kkdhhWick/UjvEwRchgoetXqTK14INLqbGm5U2TzBkBNF3nQtLYm8ovxSkQWArjEQvftCKryjZaATu3w==
 
+axios@^1.1.3:
+  version "1.1.3"
+  resolved "https://registry.yarnpkg.com/axios/-/axios-1.1.3.tgz#8274250dada2edf53814ed7db644b9c2866c1e35"
+  integrity sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==
+  dependencies:
+    follow-redirects "^1.15.0"
+    form-data "^4.0.0"
+    proxy-from-env "^1.1.0"
+
 axobject-query@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be"
@@ -2105,6 +2124,13 @@ color-name@~1.1.4:
   resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
   integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
 
+combined-stream@^1.0.8:
+  version "1.0.8"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+  integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+  dependencies:
+    delayed-stream "~1.0.0"
+
 commander@^2.20.0:
   version "2.20.3"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
@@ -2305,6 +2331,11 @@ del@^4.1.1:
     pify "^4.0.1"
     rimraf "^2.6.3"
 
+delayed-stream@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+  integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
 delegates@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
@@ -2812,6 +2843,11 @@ flatted@^3.1.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
   integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
 
+follow-redirects@^1.15.0:
+  version "1.15.2"
+  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
+  integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==
+
 for-each@~0.3.3:
   version "0.3.3"
   resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@@ -2819,6 +2855,15 @@ for-each@~0.3.3:
   dependencies:
     is-callable "^1.1.3"
 
+form-data@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
+  integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.8"
+    mime-types "^2.1.12"
+
 framer-motion@^6.2.8:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-6.5.1.tgz#802448a16a6eb764124bf36d8cbdfa6dd6b931a7"
@@ -3429,6 +3474,11 @@ jsonpointer@^5.0.0:
     array-includes "^3.1.5"
     object.assign "^4.1.3"
 
+jwt-decode@^3.1.2:
+  version "3.1.2"
+  resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
+  integrity sha512-UfpWE/VZn0iP50d8cz9NrZLM9lSWhcJ+0Gt/nm4by88UL+J1SiKN8/5dkjMmbEzwL2CAe+67GsegCbIKtbp75A==
+
 kld-affine@^2.1.1:
   version "2.1.1"
   resolved "https://registry.yarnpkg.com/kld-affine/-/kld-affine-2.1.1.tgz#e28296585f305230eaad3b1e346d3ac5ca4c72b3"
@@ -3621,6 +3671,18 @@ micromatch@^4.0.4:
     braces "^3.0.2"
     picomatch "^2.3.1"
 
[email protected]:
+  version "1.52.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+  integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+  version "2.1.35"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+  integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+  dependencies:
+    mime-db "1.52.0"
+
 minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
   version "3.1.2"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"