From 0e35da1f15965976f7e3cb45f0ba4451bb7def81 Mon Sep 17 00:00:00 2001 From: barshathakuri Date: Wed, 20 Nov 2024 16:10:30 +0545 Subject: [PATCH] Login integration --- backend | 2 +- src/App/Auth.tsx | 45 ++++++++++ src/App/index.tsx | 64 +++++++++++-- src/App/routes/common.tsx | 47 ++++++++++ src/App/routes/index.tsx | 40 +++++---- src/components/Link/index.tsx | 2 +- src/components/Navbar/i18n.json | 3 +- src/components/Navbar/index.tsx | 41 +++++++-- src/contexts/domain.tsx | 29 ++++++ src/contexts/user.tsx | 10 +-- src/hooks/{ => domain}/useAuth.ts | 0 src/hooks/domain/useUserMe.ts | 21 +++++ src/utils/constants.ts | 3 + src/utils/user.ts | 31 +++++++ src/views/Login/i18n.json | 3 +- src/views/Login/index.tsx | 144 ++++++++++++++++++++++++------ 16 files changed, 417 insertions(+), 68 deletions(-) create mode 100644 src/App/Auth.tsx create mode 100644 src/App/routes/common.tsx create mode 100644 src/contexts/domain.tsx rename src/hooks/{ => domain}/useAuth.ts (100%) create mode 100644 src/hooks/domain/useUserMe.ts create mode 100644 src/utils/user.ts diff --git a/backend b/backend index 69450f82..b83328d0 160000 --- a/backend +++ b/backend @@ -1 +1 @@ -Subproject commit 69450f82253f675cf923779396f46cb2e809dcf8 +Subproject commit b83328d0dcb3c1d037b92af0e45b1c98e3d76248 diff --git a/src/App/Auth.tsx b/src/App/Auth.tsx new file mode 100644 index 00000000..0cbc1ee7 --- /dev/null +++ b/src/App/Auth.tsx @@ -0,0 +1,45 @@ +import { + Fragment, + type ReactElement, +} from 'react'; +import { Navigate } from 'react-router-dom'; + +import useAuth from '#hooks/domain/useAuth'; + +import { type ExtendedProps } from './routes/common'; + +interface Props { + children: ReactElement, + context: ExtendedProps, + absolutePath: string, +} +function Auth(props: Props) { + const { + context, + children, + absolutePath, + } = props; + + const { isAuthenticated } = useAuth(); + + if (context.visibility === 'is-authenticated' && !isAuthenticated) { + return ( + + ); + } + if (context.visibility === 'is-not-authenticated' && isAuthenticated) { + return ( + + ); + } + + return ( + + {children} + + ); +} + +export default Auth; diff --git a/src/App/index.tsx b/src/App/index.tsx index 8e16d45e..e5176385 100644 --- a/src/App/index.tsx +++ b/src/App/index.tsx @@ -27,9 +27,17 @@ import mapboxgl from 'mapbox-gl'; import { mapboxToken } from '#config'; import RouteContext from '#contexts/route'; -import { KEY_LANGUAGE_STORAGE } from '#utils/constants'; +import UserContext, { + UserAuth, + UserContextProps, +} from '#contexts/user'; +import { + KEY_LANGUAGE_STORAGE, + KEY_USER_STORAGE, +} from '#utils/constants'; import { getFromStorage, + removeFromStorage, setToStorage, } from '#utils/localStorage'; @@ -73,6 +81,48 @@ function App() { [], ); + // AUTH + + const [userAuth, setUserAuth] = useState(); + + const hydrateUserAuth = useCallback(() => { + const userDetailsFromStorage = getFromStorage(KEY_USER_STORAGE); + if (userDetailsFromStorage) { + setUserAuth(userDetailsFromStorage); + } + }, []); + + const removeUserAuth = useCallback(() => { + removeFromStorage(KEY_USER_STORAGE); + setUserAuth(undefined); + }, []); + + const setAndStoreUserAuth = useCallback((newUserDetails: UserAuth) => { + setUserAuth(newUserDetails); + setToStorage( + KEY_USER_STORAGE, + newUserDetails, + ); + }, []); + + // Hydration + useEffect(() => { + hydrateUserAuth(); + + const language = getFromStorage(KEY_LANGUAGE_STORAGE); + setCurrentLanguage(language ?? 'en'); + }, [hydrateUserAuth]); + + const userContextValue = useMemo( + () => ({ + userAuth, + hydrateUserAuth, + setUserAuth: setAndStoreUserAuth, + removeUserAuth, + }), + [userAuth, hydrateUserAuth, setAndStoreUserAuth, removeUserAuth], + ); + const registerLanguageNamespace = useCallback( (namespace: string, fallbackStrings: Record) => { setStrings( @@ -187,11 +237,13 @@ function App() { return ( - - - - - + + + + + + + ); } diff --git a/src/App/routes/common.tsx b/src/App/routes/common.tsx new file mode 100644 index 00000000..bfad45ea --- /dev/null +++ b/src/App/routes/common.tsx @@ -0,0 +1,47 @@ +import { + type MyInputIndexRouteObject, + type MyInputNonIndexRouteObject, + type MyOutputIndexRouteObject, + type MyOutputNonIndexRouteObject, + wrapRoute, +} from '#utils/routes'; +import { Component as RootLayout } from '#views/RootLayout'; + +import Auth from '../Auth'; +import PageError from '../PageError'; + +export type ExtendedProps = { + title: string, + visibility: 'is-authenticated' | 'is-not-authenticated' | 'anything', + permissions?: ( + params: Record | undefined | null, + ) => boolean; +}; + +interface CustomWrapRoute { + ( + myRouteOptions: MyInputIndexRouteObject + ): MyOutputIndexRouteObject + ( + myRouteOptions: MyInputNonIndexRouteObject + ): MyOutputNonIndexRouteObject +} + +export const customWrapRoute: CustomWrapRoute = wrapRoute; + +// NOTE: We should not use layout or index routes in links + +export const rootLayout = customWrapRoute({ + path: '/', + errorElement: , + component: { + eagerLoad: true, + render: RootLayout, + props: {}, + }, + wrapperComponent: Auth, + context: { + title: 'IFRC Alert Hub', + visibility: 'anything', + }, +}); diff --git a/src/App/routes/index.tsx b/src/App/routes/index.tsx index e8d8987c..e194c249 100644 --- a/src/App/routes/index.tsx +++ b/src/App/routes/index.tsx @@ -6,11 +6,13 @@ import { MyOutputIndexRouteObject, MyOutputNonIndexRouteObject, unwrapRoute, - wrapRoute, } from '#utils/routes'; -import { Component as RootLayout } from '#views/RootLayout'; -import PageError from '../PageError'; +import Auth from '../Auth'; +import { + customWrapRoute, + rootLayout, +} from './common'; // NOTE: setting default ExtendedProps export type ExtendedProps = { @@ -27,22 +29,6 @@ export interface MyWrapRoute { ): MyOutputNonIndexRouteObject } -const customWrapRoute: MyWrapRoute = wrapRoute; - -const rootLayout = customWrapRoute({ - path: '/', - errorElement: , - component: { - render: RootLayout, - eagerLoad: true, - props: {}, - }, - context: { - title: 'IFRC Alert Hub', - visibility: 'anything', - }, -}); - type DefaultHomeChild = 'map'; const homeLayout = customWrapRoute({ parent: rootLayout, @@ -51,6 +37,7 @@ const homeLayout = customWrapRoute({ render: () => import('#views/Home'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', @@ -64,6 +51,7 @@ const mySubscription = customWrapRoute({ render: () => import('#views/MySubscription'), props: {}, }, + wrapperComponent: Auth, context: { title: 'My Subscriptions', // TODO: Change visibility after login feature @@ -82,6 +70,7 @@ const homeIndex = customWrapRoute({ replace: true, }, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub', visibility: 'anything', @@ -95,6 +84,7 @@ const homeMap = customWrapRoute({ render: () => import('#views/Home/AlertsMap'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Map', visibility: 'anything', @@ -108,6 +98,7 @@ const homeTable = customWrapRoute({ render: () => import('#views/Home/AlertsTable'), props: {}, }, + wrapperComponent: Auth, context: { title: 'IFRC Alert Hub - Table', visibility: 'anything', @@ -121,6 +112,7 @@ const preferences = customWrapRoute({ render: () => import('#views/Preferences'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Preferences', visibility: 'anything', @@ -134,6 +126,7 @@ const historicalAlerts = customWrapRoute({ render: () => import('#views/HistoricalAlerts'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Historical Alerts', visibility: 'anything', @@ -147,6 +140,7 @@ const about = customWrapRoute({ render: () => import('#views/About'), props: {}, }, + wrapperComponent: Auth, context: { title: 'About', visibility: 'anything', @@ -160,6 +154,7 @@ const resources = customWrapRoute({ render: () => import('#views/Resources'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Resources', visibility: 'anything', @@ -173,6 +168,7 @@ const alertDetails = customWrapRoute({ render: () => import('#views/AlertDetails'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Alert Details', visibility: 'anything', @@ -186,6 +182,7 @@ const allSourcesFeeds = customWrapRoute({ render: () => import('#views/AllSourcesFeeds'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Sources Feeds', visibility: 'anything', @@ -199,6 +196,7 @@ const pageNotFound = customWrapRoute({ render: () => import('#views/PageNotFound'), props: {}, }, + wrapperComponent: Auth, context: { title: '404', visibility: 'anything', @@ -212,6 +210,7 @@ const login = customWrapRoute({ render: () => import('#views/Login'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Login', visibility: 'is-not-authenticated', @@ -225,6 +224,7 @@ const recoverAccount = customWrapRoute({ render: () => import('#views/RecoverAccount'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Recover Account', visibility: 'is-not-authenticated', @@ -238,6 +238,7 @@ const resendValidationEmail = customWrapRoute({ render: () => import('#views/ResendValidationEmail'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Resend Validation Email', visibility: 'is-not-authenticated', @@ -251,6 +252,7 @@ const cookiePolicy = customWrapRoute({ render: () => import('#views/CookiePolicy'), props: {}, }, + wrapperComponent: Auth, context: { title: 'Cookie Policy', visibility: 'anything', diff --git a/src/components/Link/index.tsx b/src/components/Link/index.tsx index b85ac36a..1f26d4c5 100644 --- a/src/components/Link/index.tsx +++ b/src/components/Link/index.tsx @@ -21,7 +21,7 @@ import { } from '@togglecorp/fujs'; import RouteContext from '#contexts/route'; -import useAuth from '#hooks/useAuth'; +import useAuth from '#hooks/domain/useAuth'; import { type WrappedRoutes } from '../../App/routes'; diff --git a/src/components/Navbar/i18n.json b/src/components/Navbar/i18n.json index 7ffcd233..70550dbd 100644 --- a/src/components/Navbar/i18n.json +++ b/src/components/Navbar/i18n.json @@ -7,6 +7,7 @@ "appResources": "Resources", "headerMenuHome": "Home", "headerMenuMySubscription": "My Subscriptions", - "historicalAlerts": "Historical Alerts" + "historicalAlerts": "Historical Alerts", + "userMenuLogout":"Logout" } } diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index eb1ebc2b..6a0916ef 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -1,4 +1,9 @@ import { + useCallback, + useContext, +} from 'react'; +import { + Button, Heading, NavigationTabList, PageContainer, @@ -9,6 +14,8 @@ import { _cs } from '@togglecorp/fujs'; import goLogo from '#assets/icons/go-logo-2020.svg'; import Link from '#components/Link'; import NavigationTab from '#components/NavigationTab'; +import UserContext from '#contexts/user'; +import useAuth from '#hooks/domain/useAuth'; import LangaugeDropdown from './LanguageDropdown'; @@ -21,6 +28,17 @@ interface Props { function Navbar(props: Props) { const { className } = props; const strings = useTranslation(i18n); + const { isAuthenticated } = useAuth(); + + const { + removeUserAuth: removeUser, + } = useContext(UserContext); + + const handleLogoutConfirm = useCallback(() => { + removeUser(); + window.location.reload(); + }, [removeUser]); + return (