diff --git a/package-lock.json b/package-lock.json index 8291946..8ca393d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,8 @@ "react-feather": "^2.0.10", "react-responsive": "^10.0.0", "react-router-dom": "^6.26.2", - "vite-express": "0.16.0" + "vite-express": "0.16.0", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.2", @@ -5728,6 +5729,34 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zustand": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.2.tgz", + "integrity": "sha512-8qNdnJVJlHlrKXi50LDqqUNmUbuBjoKLrYQBnoChIbVph7vni+sY+YpvdjXG9YLd/Bxr6scMcR+rm5H3aSqPaw==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } } } } diff --git a/package.json b/package.json index 9030925..1a88de4 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "react-feather": "^2.0.10", "react-responsive": "^10.0.0", "react-router-dom": "^6.26.2", - "vite-express": "0.16.0" + "vite-express": "0.16.0", + "zustand": "^5.0.2" }, "devDependencies": { "@types/react": "^18.3.2", diff --git a/src/App.tsx b/src/App.tsx index a65eef4..68fff38 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,12 +17,16 @@ const App = () => { }> } /> - } /> - } /> - } /> - } /> - } />} /> - } />} /> + }> + } /> + + }> + } /> + } /> + + }> + } /> + } /> diff --git a/src/components/ProtectedAuthorizedUserRoute.tsx b/src/components/ProtectedAuthorizedUserRoute.tsx index 7ff419f..108b0d2 100644 --- a/src/components/ProtectedAuthorizedUserRoute.tsx +++ b/src/components/ProtectedAuthorizedUserRoute.tsx @@ -1,29 +1,43 @@ -import React, { useEffect, useState } from 'react' +import { useEffect, useState } from 'react' import NotFound from '../pages/NotFound/NotFound.tsx' import { User } from '../services/userProfile' import { isAuthorizedToCreateTeam } from '../services/createTeam' -import { Effect } from 'effect' +import { Effect, Option as O } from 'effect' import { isDaplaAdmin } from '../utils/services' +import { option } from '../utils/utils' +import { Skeleton } from '@mui/material' +import PageLayout from './PageLayout/PageLayout.tsx' +import { Outlet } from 'react-router-dom' +import { useUserProfileStore } from '../services/store.ts' -export interface Props { - component: React.ReactElement -} +const ProtectedAuthorizedUserRoute = () => { + // O.none() here represents the loading state + const [oIsAuthorized, setIsAuthorized] = useState>(O.none()) + const maybeUser: O.Option = useUserProfileStore((state) => state.loggedInUser) -const ProtectedAuthorizedUserRoute = ({ component }: Props) => { - const [isAuthorized, setIsAuthorized] = useState(false) useEffect(() => { - const userProfileItem = localStorage.getItem('userProfile') - if (!userProfileItem) return + Effect.gen(function* () { + const user: User = yield* O.match(maybeUser, { + onNone: () => Effect.fail(new Error('User not logged in!')), + onSome: (user) => Effect.succeed(user), + }) - const user = JSON.parse(userProfileItem) as User - if (!user) return + const daplaAdmin: boolean = yield* Effect.promise(() => isDaplaAdmin(user.principal_name)) - Effect.promise(() => isDaplaAdmin(user.principal_name)) - .pipe(Effect.runPromise) - .then((isDaplaAdmin: boolean) => setIsAuthorized(isAuthorizedToCreateTeam(isDaplaAdmin, user.job_title))) + yield* Effect.sync(() => setIsAuthorized(O.some(isAuthorizedToCreateTeam(daplaAdmin, user.job_title)))) + }).pipe(Effect.runPromise) }, []) - return isAuthorized ? component : + return option( + oIsAuthorized, + () => ( + } + /> + ), + (isAuthorized) => (isAuthorized ? : ) + ) } export default ProtectedAuthorizedUserRoute diff --git a/src/components/ProtectedRoute.tsx b/src/components/ProtectedRoute.tsx index 104a90c..757792e 100644 --- a/src/components/ProtectedRoute.tsx +++ b/src/components/ProtectedRoute.tsx @@ -5,11 +5,13 @@ import { fetchUserInformationFromAuthToken } from '../utils/services' import { Cause, Effect, Option as O } from 'effect' import { customLogger } from '../utils/logger.ts' import { ApiError } from '../utils/services.ts' +import { useUserProfileStore } from '../services/store.ts' import { User } from '../services/userProfile.ts' const ProtectedRoute = () => { const [isAuthenticated, setIsAuthenticated] = useState(false) + const setUser = useUserProfileStore((state) => state.setUser) const navigate = useNavigate() const from = location.pathname @@ -20,8 +22,8 @@ const ProtectedRoute = () => { Effect.flatMap((x) => (x instanceof ApiError ? Effect.fail(x) : Effect.succeed(x))), Effect.map(JSON.stringify) ) - yield* Effect.logInfo(`UserProfile set in localStorage: ${userProfile}`) yield* Effect.sync(() => localStorage.setItem('userProfile', userProfile)) + yield* Effect.sync(() => setUser(userProfile)) yield* Effect.sync(() => setIsAuthenticated(true)) }).pipe(Effect.provide(customLogger)) @@ -34,7 +36,10 @@ const ProtectedRoute = () => { onSome: (userProfile) => // invalidate cached user profile if 'job_title' field is missing userProfile.job_title - ? Effect.sync(() => setIsAuthenticated(true)) + ? Effect.zip( + Effect.sync(() => setIsAuthenticated(true)), + Effect.sync(() => setUser(userProfile)) + ) : Effect.zipRight( Effect.logInfo("'job_title' field missing, invalidating UserProfile cache"), fetchUserProfile()