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..1aa7a1d 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..affa812 100644
--- a/src/components/ProtectedRoute.tsx
+++ b/src/components/ProtectedRoute.tsx
@@ -5,27 +5,28 @@ 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 setLoggedInUser = useUserProfileStore((state) => state.setLoggedInUser)
const navigate = useNavigate()
const from = location.pathname
- const fetchUserProfile = (): Effect.Effect =>
- Effect.gen(function* () {
- const userProfileData = yield* Effect.promise(fetchUserInformationFromAuthToken)
- const userProfile = yield* Effect.tryPromise(() => getUserProfile(userProfileData.email)).pipe(
- 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(() => setIsAuthenticated(true))
- }).pipe(Effect.provide(customLogger))
-
useEffect(() => {
+ const fetchUserProfile = (): Effect.Effect =>
+ Effect.gen(function* () {
+ const userProfileData = yield* Effect.promise(fetchUserInformationFromAuthToken)
+ const userProfile = yield* Effect.tryPromise(() => getUserProfile(userProfileData.email)).pipe(
+ Effect.flatMap((x) => (x instanceof ApiError ? Effect.fail(x) : Effect.succeed(x)))
+ )
+ yield* Effect.sync(() => localStorage.setItem('userProfile', JSON.stringify(userProfile)))
+ yield* Effect.sync(() => setLoggedInUser(userProfile))
+ yield* Effect.sync(() => setIsAuthenticated(true))
+ }).pipe(Effect.provide(customLogger))
+
const cachedUserProfile: O.Option = O.fromNullable(localStorage.getItem('userProfile')).pipe(
O.flatMap(O.liftThrowable(JSON.parse))
)
@@ -34,13 +35,16 @@ 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(() => setLoggedInUser(userProfile))
+ )
: Effect.zipRight(
Effect.logInfo("'job_title' field missing, invalidating UserProfile cache"),
fetchUserProfile()
).pipe(Effect.provide(customLogger)),
}).pipe(Effect.runPromise)
- }, [from, navigate])
+ }, [from, navigate, setLoggedInUser])
return isAuthenticated ? : null
}
diff --git a/src/services/store.ts b/src/services/store.ts
new file mode 100644
index 0000000..d0bb40c
--- /dev/null
+++ b/src/services/store.ts
@@ -0,0 +1,28 @@
+import { create } from 'zustand'
+import { subscribeWithSelector } from 'zustand/middleware'
+import { User } from '../services/userProfile.ts'
+import { Effect, Option as O } from 'effect'
+
+import { customLogger } from '../utils/logger.ts'
+
+type UserProfileStoreState = {
+ loggedInUser: O.Option
+}
+
+type UserProfileStoreActions = {
+ setLoggedInUser: (user: User) => void
+}
+
+export type UserProfileStore = UserProfileStoreState & UserProfileStoreActions
+
+export const useUserProfileStore = create()(
+ subscribeWithSelector((set) => ({
+ loggedInUser: O.none(),
+ setLoggedInUser: (user: User) => set(() => ({ loggedInUser: O.some(user) })),
+ }))
+)
+
+useUserProfileStore.subscribe(
+ (state) => state.loggedInUser,
+ (user) => Effect.log('USER LOGGED IN:', O.getOrNull(user)).pipe(Effect.provide(customLogger), Effect.runPromise)
+)