From 3b6fde153e0be0fe94ac77a34627df0aa7acf608 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Thu, 30 Mar 2023 17:14:07 -0400 Subject: [PATCH] authentication flow --- pnpm-lock.yaml | 6 +-- src/App.tsx | 60 ++++++++++++++++++++++---- src/assets/images/logo/Logo-Blue.svg | 11 +++++ src/assets/images/logo/Logo-White.svg | 11 +++++ src/components/AuthenticationGuard.tsx | 17 ++++++++ src/components/Container.tsx | 1 + src/contexts/Auth.tsx | 25 ----------- src/contexts/Theme.tsx | 4 +- src/hooks/useAuth.ts | 6 --- src/index.tsx | 14 ++---- src/pages/LoadingPage.tsx | 31 +++++++++++++ src/pages/LoginPage.tsx | 35 +++++++-------- src/pages/RootPage.tsx | 11 ++++- src/stores/AppStore.ts | 10 +++-- src/utils/RequireAuth.tsx | 15 ------- 15 files changed, 163 insertions(+), 94 deletions(-) create mode 100644 src/assets/images/logo/Logo-Blue.svg create mode 100644 src/assets/images/logo/Logo-White.svg create mode 100644 src/components/AuthenticationGuard.tsx delete mode 100644 src/contexts/Auth.tsx delete mode 100644 src/hooks/useAuth.ts create mode 100644 src/pages/LoadingPage.tsx delete mode 100644 src/utils/RequireAuth.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10b347f..f740b28 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,7 +43,7 @@ dependencies: '@fontsource/roboto': 4.5.8 '@mattjennings/react-modal-stack': 1.0.4_react@18.2.0 '@mui/material': 5.11.13_xqeqsl5kvjjtyxwyi3jhw3yuli - '@puyodead1/fosscord-ts': github.com/Puyodead1/fosscord.ts/d736433fa8e05d8b761eb352c813d93f7afb0baf + '@puyodead1/fosscord-ts': github.com/Puyodead1/fosscord.ts/eb44aa8023076f6e18f22354143fe1032062fa67 '@testing-library/jest-dom': 5.16.5 '@testing-library/react': 13.4.0_biqbaboplfbrettd7655fr4n2y '@testing-library/user-event': 13.5.0 @@ -11233,8 +11233,8 @@ packages: - debug dev: false - github.com/Puyodead1/fosscord.ts/d736433fa8e05d8b761eb352c813d93f7afb0baf: - resolution: {tarball: https://codeload.github.com/Puyodead1/fosscord.ts/tar.gz/d736433fa8e05d8b761eb352c813d93f7afb0baf} + github.com/Puyodead1/fosscord.ts/eb44aa8023076f6e18f22354143fe1032062fa67: + resolution: {tarball: https://codeload.github.com/Puyodead1/fosscord.ts/tar.gz/eb44aa8023076f6e18f22354143fe1032062fa67} name: '@puyodead1/fosscord-ts' version: 0.0.1 prepare: true diff --git a/src/App.tsx b/src/App.tsx index 67ecd7c..31d6999 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,22 +1,64 @@ -import { Route, Routes } from "react-router-dom"; +import { observer } from "mobx-react-lite"; +import React from "react"; +import { Route, Routes, useNavigate } from "react-router-dom"; +import secureLocalStorage from "react-secure-storage"; +import { AuthenticationGuard } from "./components/AuthenticationGuard"; +import LoadingPage from "./pages/LoadingPage"; import LoginPage from "./pages/LoginPage"; import NotFoundPage from "./pages/NotFound"; import RegistrationPage from "./pages/RegistrationPage"; import RootPage from "./pages/RootPage"; -import RequireAuth from "./utils/RequireAuth"; +import { useAppStore } from "./stores/AppStore"; function App() { + const app = useAppStore(); + const navigate = useNavigate(); + const [isLoading, setLoading] = React.useState(true); + + React.useEffect(() => { + const token = secureLocalStorage.getItem("token"); + if (token) { + app.api.loginWithToken(token as string).then(() => { + setLoading(false); + }); + } else { + // set timeout to prevent flashing + setTimeout(() => { + setLoading(false); + }, 1000); + } + }, []); + + // handles token changes + React.useEffect(() => { + if (!app.api.token && !isLoading) { + console.log("TOKEN REMOVED"); + // remove token + secureLocalStorage.removeItem("token"); + // navigate to login page if token is removed + navigate("/login", { replace: true }); + } + + if (app.api.token) { + console.log("TOKEN ADDED"); + // save token + secureLocalStorage.setItem("token", app.api.token); + // navigate to root page if token is added + navigate("/", { replace: true }); + } + }, [app.api.token, isLoading]); + + if (isLoading) { + return ; + } + return ( - - - } + element={} /> - } /> } /> } /> @@ -24,4 +66,4 @@ function App() { ); } -export default App; +export default observer(App); diff --git a/src/assets/images/logo/Logo-Blue.svg b/src/assets/images/logo/Logo-Blue.svg new file mode 100644 index 0000000..65efd86 --- /dev/null +++ b/src/assets/images/logo/Logo-Blue.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/assets/images/logo/Logo-White.svg b/src/assets/images/logo/Logo-White.svg new file mode 100644 index 0000000..af3ba30 --- /dev/null +++ b/src/assets/images/logo/Logo-White.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/components/AuthenticationGuard.tsx b/src/components/AuthenticationGuard.tsx new file mode 100644 index 0000000..8b0fd08 --- /dev/null +++ b/src/components/AuthenticationGuard.tsx @@ -0,0 +1,17 @@ +import { Navigate } from "react-router-dom"; +import { useAppStore } from "../stores/AppStore"; + +interface Props { + component: React.FC; +} + +export const AuthenticationGuard = ({ component }: Props) => { + const app = useAppStore(); + + if (!app.api.token) { + return ; + } + + const Component = component; + return ; +}; diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 19cb6eb..c21562c 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -3,4 +3,5 @@ import styled from "styled-components"; export default styled.div` background-color: var(--tertiary); color: var(--text); + overflow: hidden; `; diff --git a/src/contexts/Auth.tsx b/src/contexts/Auth.tsx deleted file mode 100644 index 68ab6a6..0000000 --- a/src/contexts/Auth.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; - -interface AuthContextType { - user: any; - login: () => void; - logout: () => void; -} - -export const AuthContext = React.createContext(null!); - -export function AuthProvider({ children }: { children: React.ReactNode }) { - let [user, setUser] = React.useState(null); - - let login = () => { - setUser("test"); - }; - - let logout = () => { - setUser(null); - }; - - let value = { user, login, logout }; - - return {children}; -} diff --git a/src/contexts/Theme.tsx b/src/contexts/Theme.tsx index cd82f3a..085bec3 100644 --- a/src/contexts/Theme.tsx +++ b/src/contexts/Theme.tsx @@ -40,7 +40,7 @@ export type Theme = Overrides & { export const ThemePresets: Record = { light: { brandPrimary: "#0185ff", - brandSecondary: "#000115", + brandSecondary: "#ffffff", primary: "#ede8e7", primaryAlt: "", secondary: "#ebe5e4", @@ -67,7 +67,7 @@ export const ThemePresets: Record = { }, dark: { brandPrimary: "#0185ff", - brandSecondary: "#000115", + brandSecondary: "#ffffff", primary: "#232120", primaryAlt: "#312e2d", secondary: "#1b1918", diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts deleted file mode 100644 index 645e158..0000000 --- a/src/hooks/useAuth.ts +++ /dev/null @@ -1,6 +0,0 @@ -import React from "react"; -import { AuthContext } from "../contexts/Auth"; - -export default function useAuth() { - return React.useContext(AuthContext); -} diff --git a/src/index.tsx b/src/index.tsx index 5caf8d2..ca22518 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,11 +2,9 @@ import "@fontsource/roboto/300.css"; import "@fontsource/roboto/400.css"; import "@fontsource/roboto/500.css"; import "@fontsource/roboto/700.css"; -import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; -import { AuthProvider } from "./contexts/Auth"; import Theme from "./contexts/Theme"; import "./index.css"; @@ -14,12 +12,8 @@ const root = ReactDOM.createRoot( document.getElementById("root") as HTMLElement ); root.render( - - - - - - - - + + + + ); diff --git a/src/pages/LoadingPage.tsx b/src/pages/LoadingPage.tsx new file mode 100644 index 0000000..d806a4a --- /dev/null +++ b/src/pages/LoadingPage.tsx @@ -0,0 +1,31 @@ +import { observer } from "mobx-react-lite"; +import PulseLoader from "react-spinners/PulseLoader"; +import styled from "styled-components"; +import { ReactComponent as SpacebarLogoBlue } from "../assets/images/logo/Logo-Blue.svg"; +import Container from "../components/Container"; + +const Wrapper = styled.div` + justify-content: center; + align-items: center; + display: flex; + height: 100vh; + flex-direction: column; +`; + +const SpacebarLogo = styled(SpacebarLogoBlue)` + height: 120px; + margin-bottom: 32px; +`; + +function LoadingPage() { + return ( + + + + + + + ); +} + +export default observer(LoadingPage); diff --git a/src/pages/LoginPage.tsx b/src/pages/LoginPage.tsx index 9108cdf..7da9c1f 100644 --- a/src/pages/LoginPage.tsx +++ b/src/pages/LoginPage.tsx @@ -153,26 +153,23 @@ function LoginPage() { } = useForm(); const onSubmit = handleSubmit((data) => { - app.api - .login(data) - .then(app.setToken) - .catch((e) => { - if (e instanceof MFAError) { - console.log("MFA Required", e); - } else if (e instanceof CaptchaError) { - console.log("Captcha Required", e); - } else if (e instanceof APIError) { - console.log("APIError", e.message, e.code, e.fieldErrors); - e.fieldErrors.forEach((fieldError) => { - setError(fieldError.field as any, { - type: "manual", - message: fieldError.error, - }); + app.api.login(data).catch((e) => { + if (e instanceof MFAError) { + console.log("MFA Required", e); + } else if (e instanceof CaptchaError) { + console.log("Captcha Required", e); + } else if (e instanceof APIError) { + console.log("APIError", e.message, e.code, e.fieldErrors); + e.fieldErrors.forEach((fieldError) => { + setError(fieldError.field as any, { + type: "manual", + message: fieldError.error, }); - } else { - console.log("General Error", e); - } - }); + }); + } else { + console.log("General Error", e); + } + }); }); return ( diff --git a/src/pages/RootPage.tsx b/src/pages/RootPage.tsx index 1765c3d..5f7adbe 100644 --- a/src/pages/RootPage.tsx +++ b/src/pages/RootPage.tsx @@ -1,7 +1,16 @@ +import { observer } from "mobx-react-lite"; import Container from "../components/Container"; +import { useAppStore } from "../stores/AppStore"; +import LoadingPage from "./LoadingPage"; function RootPage() { + const app = useAppStore(); + + if (!app.ready) { + return ; + } + return RootPage; } -export default RootPage; +export default observer(RootPage); diff --git a/src/stores/AppStore.ts b/src/stores/AppStore.ts index f258e5b..b9aaf7a 100644 --- a/src/stores/AppStore.ts +++ b/src/stores/AppStore.ts @@ -1,6 +1,5 @@ import { Client } from "@puyodead1/fosscord-ts"; -import { makeAutoObservable, observable } from "mobx"; -import secureLocalStorage from "react-secure-storage"; +import { makeAutoObservable, observable, runInAction } from "mobx"; import ThemeStore from "./ThemeStore"; export default class AppStore { @@ -20,12 +19,15 @@ export default class AppStore { this.api.on("debug", console.debug); this.api.on("warn", console.warn); this.api.on("error", console.error); + this.api.on("ready", this.onReady.bind(this)); makeAutoObservable(this); } - public setToken(token: string) { - secureLocalStorage.setItem("token", token); + onReady() { + runInAction(() => { + this.ready = true; + }); } } diff --git a/src/utils/RequireAuth.tsx b/src/utils/RequireAuth.tsx deleted file mode 100644 index 1cd62c1..0000000 --- a/src/utils/RequireAuth.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Navigate, useLocation } from "react-router-dom"; -import useAuth from "../hooks/useAuth"; - -function RequireAuth({ children }: { children: JSX.Element }) { - let auth = useAuth(); - let location = useLocation(); - - if (!auth.user) { - return ; - } - - return children; -} - -export default RequireAuth;