From aab0c1ac76675f6274e02e7caace48defe627ab8 Mon Sep 17 00:00:00 2001 From: Soeren Wegener Date: Tue, 12 Nov 2024 01:25:03 +0100 Subject: [PATCH] Better session handling an dless flickering --- src/App.tsx | 28 ++++++++++++++---------- src/AuthContext.tsx | 51 ++++++++++++++++++++++++------------------- src/AuthService.ts | 33 ---------------------------- src/Home.tsx | 4 ---- src/Login.tsx | 11 ++++++---- src/LoginCallback.tsx | 20 +++++++++-------- src/NavBar.tsx | 13 +++++------ src/NotFound.tsx | 27 +++++++++++++++++++++++ src/RequireAuth.tsx | 5 +++-- src/Spinner.css | 33 ++++++++++++++++++++++++++++ src/Spinner.tsx | 9 ++++++++ src/main.tsx | 5 ++++- 12 files changed, 145 insertions(+), 94 deletions(-) delete mode 100644 src/AuthService.ts create mode 100644 src/NotFound.tsx create mode 100644 src/Spinner.css create mode 100644 src/Spinner.tsx diff --git a/src/App.tsx b/src/App.tsx index 6856c76..5afbe4d 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,30 +1,36 @@ import './App.css' // import { Navigate, useLocation, useNavigate } from "react-router-dom"; import Login from "./Login.tsx"; -import {AuthProvider} from "./AuthContext.tsx"; -import {Route, Routes} from "react-router-dom"; +import {useAuth} from "./AuthContext.tsx"; +import {Navigate, Route, Routes} from "react-router-dom"; import Home from "./Home.tsx"; -import RequireAuth from "./RequireAuth.tsx"; import LoginCallback from "./LoginCallback.tsx"; import NavBar from "./NavBar.tsx"; - +import NotFound from "./NotFound.tsx"; function App() { - return + const {user} = useAuth(); + + const AnonRoutes = <> + }/> + + + const LoggedInRoutes = <> + }/> + + + return <>
- - - }/> - - + {user === undefined ? null : user ? LoggedInRoutes : AnonRoutes} }/> }/> + }/>
-
+ // const auth = useAuth(); // // let navigate = useNavigate(); diff --git a/src/AuthContext.tsx b/src/AuthContext.tsx index bc29baa..3057558 100644 --- a/src/AuthContext.tsx +++ b/src/AuthContext.tsx @@ -1,50 +1,55 @@ import './App.css' -import {User} from 'oidc-client-ts' +import {User, UserManager, WebStorageStateStore} from 'oidc-client-ts' // import { Navigate, useLocation, useNavigate } from "react-router-dom"; -import {createContext, useContext, useState} from "react"; -import AuthService from "./AuthService.ts"; +import {createContext, useContext, useEffect, useMemo, useState} from "react"; interface AuthContextType { - user: null | User; - login: () => void; - logout: () => void; - loginCallback: () => Promise; + user: undefined | null | User; + login: () => Promise; + logout: () => Promise; + loginCallback: () => Promise; } const AuthContext = createContext({ user: null, - login: () => { + login: async () => { console.error("login not implemented. Did you forget to wrap your app in an AuthProvider?"); }, - logout: () => { + logout: async () => { console.error("logout not implemented. Did you forget to wrap your app in an AuthProvider?"); }, loginCallback: async () => { console.error("loginCallback not implemented. Did you forget to wrap your app in an AuthProvider?"); - return null; } }); const useAuth = () => useContext(AuthContext); function AuthProvider({children}: { children: React.ReactNode }) { - const [user, setUser] = useState( - JSON.parse( - sessionStorage.getItem("session") || "null" - ) || undefined - ); - - const authService = new AuthService(); + const [user, setUser] = useState(undefined); + const userManager = useMemo(() => new UserManager({ + authority: "https://login.flipdot.org/realms/flipdot", + client_id: "flipdot-app-dashboard", + redirect_uri: window.location.origin + "/login/callback", + response_type: "code", + userStore: new WebStorageStateStore({store: window.localStorage}), + }), []); + + useEffect(() => { + userManager.getUser().then(setUser); + }, [userManager]); const loginCallback = async () => { - const authedUser = await authService.loginCallback(); - setUser(authedUser); - return authedUser; + setUser(await userManager.signinRedirectCallback()); }; - - const login = () => authService.login(); + const login = async () => { + await userManager.signinRedirect(); + setUser(await userManager.getUser()); + } const logout = async () => { - await authService.logout(); + // only logout in this application, not on the oidc server + await userManager.revokeTokens(); + await userManager.removeUser(); setUser(null); } diff --git a/src/AuthService.ts b/src/AuthService.ts deleted file mode 100644 index 56fc6ab..0000000 --- a/src/AuthService.ts +++ /dev/null @@ -1,33 +0,0 @@ -import {UserManager} from "oidc-client-ts"; - -export default class AuthService { - private userManager: UserManager; - - constructor() { - this.userManager = new UserManager({ - authority: "https://login.flipdot.org/realms/flipdot", - client_id: "flipdot-app-dashboard", - redirect_uri: window.location.origin + "/login/callback", - response_type: "code", - }) - } - - getUser() { - return this.userManager.getUser(); - } - - login() { - return this.userManager.signinRedirect(); - } - - loginCallback() { - return this.userManager.signinRedirectCallback(); - } - - logout() { - // only logout in this application, not on the oidc server - return this.userManager.revokeTokens().then(() => { - return this.userManager.removeUser(); - }); - } -} diff --git a/src/Home.tsx b/src/Home.tsx index 8e3fe25..2800571 100644 --- a/src/Home.tsx +++ b/src/Home.tsx @@ -14,10 +14,6 @@ function Home() { Hier würdest du jetzt eine Liste alle flipdot Apps sehen. Ich bin aber noch nicht fertig, sorry :)

-

- Übrigens bleibt man auch noch nicht eingeloggt wenn man die Seite neu lädt. - Upsi. -

https://github.com/flipdot/app-dashboard diff --git a/src/Login.tsx b/src/Login.tsx index ffb33a6..6194cfc 100644 --- a/src/Login.tsx +++ b/src/Login.tsx @@ -2,20 +2,23 @@ import flipdotLogo from './assets/flipdot.svg' import './App.css' import {useAuth} from "./AuthContext.tsx"; import {useNavigate} from "react-router-dom"; +import {useEffect} from "react"; function Login() { const auth = useAuth(); const navigate = useNavigate(); - if (auth.user) { - navigate("/", {replace: true}); - } + useEffect(() => { + if (auth.user) { + navigate("/", {replace: true}); + } + }, [auth, navigate]); return ( <>
- { + + useEffect(() => { + auth.loginCallback().then(() => { navigate("/", {replace: true}); - } - ).catch( - () => { + }).catch(() => { + console.error("Login callback failed"); navigate("/login", {replace: true}); - } - ); - return
-

Logging in…

-
; + }); + }); + + return ; } export default LoginCallback; \ No newline at end of file diff --git a/src/NavBar.tsx b/src/NavBar.tsx index a5950e9..5f3a6a6 100644 --- a/src/NavBar.tsx +++ b/src/NavBar.tsx @@ -2,21 +2,20 @@ import {useAuth} from "./AuthContext.tsx"; import fdLogo from './assets/fd.svg'; import "./NavBar.css"; import {Link} from "react-router-dom"; +import Spinner from "./Spinner.tsx"; function NavBar() { const auth = useAuth(); - const loginButton =
  • - -
  • ; - const userMenu =
  • + const loginButton = + const userMenu = <> {auth.user?.profile.preferred_username}
    -
  • - const rightItem = auth.user ? userMenu : loginButton; + + const rightItem = auth.user === undefined ? : auth.user ? userMenu : loginButton; return ( ) diff --git a/src/NotFound.tsx b/src/NotFound.tsx new file mode 100644 index 0000000..74e033d --- /dev/null +++ b/src/NotFound.tsx @@ -0,0 +1,27 @@ +import {useAuth} from "./AuthContext.tsx"; +import {useLocation, useNavigate} from "react-router-dom"; +import {useEffect} from "react"; + +function NotFound() { + const auth = useAuth(); + const navigate = useNavigate(); + const location = useLocation(); + + useEffect(() => { + if (auth.user === null) { + navigate("/login", {replace: true, state: {from: location}}); + } + }, [auth, navigate, location]); + + if (auth.user === undefined) { + // avoids flickering + return
    + } + + return
    +

    404

    +

    Page not found

    +
    +} + +export default NotFound \ No newline at end of file diff --git a/src/RequireAuth.tsx b/src/RequireAuth.tsx index eca419f..3e2a6c0 100644 --- a/src/RequireAuth.tsx +++ b/src/RequireAuth.tsx @@ -1,10 +1,11 @@ import {useAuth} from "./AuthContext.tsx"; -import {Navigate} from "react-router-dom"; +import {Navigate, useLocation} from "react-router-dom"; function RequireAuth({children}: { children: React.ReactNode }) { const auth = useAuth(); + const location = useLocation(); const notLoggedIn = auth.user === undefined || auth.user === null; - return notLoggedIn ? : children; + return notLoggedIn ? : children; } export default RequireAuth \ No newline at end of file diff --git a/src/Spinner.css b/src/Spinner.css new file mode 100644 index 0000000..801cf02 --- /dev/null +++ b/src/Spinner.css @@ -0,0 +1,33 @@ +.spinner { + border: 0.2em solid #aaa; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s cubic-bezier(.62, .27, .5, .75) infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +.spinner.spinner-sm { + width: 0.5em; + height: 0.5em; +} + +.spinner.spinner-md { + width: 1em; + height: 1em; +} + +.spinner.spinner-lg { + width: 4em; + height: 4em; + border-width: 0.7em; +} \ No newline at end of file diff --git a/src/Spinner.tsx b/src/Spinner.tsx new file mode 100644 index 0000000..09c89a4 --- /dev/null +++ b/src/Spinner.tsx @@ -0,0 +1,9 @@ +import "./Spinner.css"; + +function Spinner({size}: { size?: "sm" | "md" | "lg" }) { + size = size || "md"; + return
    +
    ; +} + +export default Spinner; \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index 1590acf..a89a712 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,6 +5,7 @@ import App from './App.tsx' import * as Sentry from "@sentry/react"; import {BrowserRouter} from "react-router-dom"; +import {AuthProvider} from "./AuthContext.tsx"; Sentry.init({ dsn: "https://7976fc906df26e2865ad329d909f52f5@sentry.flipdot.org/6", @@ -15,7 +16,9 @@ Sentry.init({ createRoot(document.getElementById('root')!).render( - + + + , )