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
;
+ });
+ });
+
+ 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
+}
+
+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(
-
+
+
+
,
)