From 1798bec196a88e9206c5cdfc7661e58ec8f4068e Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Tue, 25 Feb 2025 13:10:23 +0300 Subject: [PATCH 1/6] added login redirect --- .../src/pages/account/login/index.jsx | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/platform/src/pages/account/login/index.jsx b/src/platform/src/pages/account/login/index.jsx index 4a28bdac20..edbf1b764d 100644 --- a/src/platform/src/pages/account/login/index.jsx +++ b/src/platform/src/pages/account/login/index.jsx @@ -18,6 +18,7 @@ import { } from '@/lib/store/services/account/LoginSlice'; import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; import { postUserLoginDetails, getUserDetails } from '@/core/apis/Account'; +import { GOOGLE_AUTH_URL } from '@/core/urls/authentication'; const MAX_RETRIES = 3; const RETRY_DELAY = 1000; @@ -110,6 +111,15 @@ const UserLogin = () => { dispatch(setUserData({ key, value })); }; + const handleGoogleLogin = async () => { + try { + // Redirect to Google auth URL + window.location.href = GOOGLE_AUTH_URL; + } catch (error) { + console.error('Login error:', error); + } + }; + return ( { +
+ +
From ee0e231dc5732a9e83942824f89d863cc22d90ac Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Tue, 25 Feb 2025 14:57:55 +0300 Subject: [PATCH 2/6] handle google redirect logic --- src/platform/package.json | 1 + src/platform/src/pages/Home/index.jsx | 91 +++++++++++++++++++++++++++ src/platform/yarn.lock | 5 ++ 3 files changed, 97 insertions(+) diff --git a/src/platform/package.json b/src/platform/package.json index 562efa32d2..d99385ea40 100644 --- a/src/platform/package.json +++ b/src/platform/package.json @@ -30,6 +30,7 @@ "fuse.js": "^7.0.0", "html2canvas": "^1.4.1", "i18n-iso-countries": "^7.7.0", + "js-cookie": "^3.0.5", "json2csv": "^6.0.0-alpha.2", "jspdf": "^2.5.2", "jspdf-autotable": "^3.8.4", diff --git a/src/platform/src/pages/Home/index.jsx b/src/platform/src/pages/Home/index.jsx index d2457eb2cf..ef9e0c2fe4 100644 --- a/src/platform/src/pages/Home/index.jsx +++ b/src/platform/src/pages/Home/index.jsx @@ -17,6 +17,18 @@ import { import HomeSkeleton from '@/components/skeletons/HomeSkeleton'; import CustomModal from '@/components/Modal/videoModals/CustomModal'; import StepProgress from '@/components/steppers/CircularStepper'; +import Cookies from 'js-cookie'; +import jwt_decode from 'jwt-decode'; +import { useRouter } from 'next/router'; +import { + setUserInfo, + setSuccess, +} from '@/lib/store/services/account/LoginSlice'; +import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; +import { getUserDetails } from '@/core/apis/Account'; + +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; // Video URL constant const ANALYTICS_VIDEO_URL = @@ -60,6 +72,7 @@ const createSteps = (handleModal, handleCardClick) => [ const Home = () => { const dispatch = useDispatch(); + const router = useRouter(); // Selectors const checkListData = useSelector((state) => state.checklists.checklist); @@ -67,6 +80,8 @@ const Home = () => { const checkListStatus = useSelector((state) => state.checklists.status); // State hooks + const [loading, setLoading] = useState(false); + const [open, setOpen] = useState(false); const [step, setStep] = useState(0); const totalSteps = 4; @@ -85,6 +100,82 @@ const Home = () => { return null; }, []); + const retryWithDelay = async (fn, retries = MAX_RETRIES) => { + try { + return await fn(); + } catch (error) { + if (retries > 0 && error.response?.status === 429) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); + return retryWithDelay(fn, retries - 1); + } + throw error; + } + }; + + // Refactored session setup logic for reuse + const setupUserSession = async (user) => { + if (!user.groups[0]?.grp_title) { + throw new Error( + 'Server error. Contact support to add you to the AirQo Organisation', + ); + } + + localStorage.setItem('loggedUser', JSON.stringify(user)); + + const preferencesResponse = await retryWithDelay(() => + dispatch(getIndividualUserPreferences({ identifier: user._id })), + ); + if (preferencesResponse.payload.success) { + const preferences = preferencesResponse.payload.preferences; + const activeGroup = preferences[0]?.group_id + ? user.groups.find((group) => group._id === preferences[0].group_id) + : user.groups.find((group) => group.grp_title === 'airqo'); + localStorage.setItem('activeGroup', JSON.stringify(activeGroup)); + } + + dispatch(setUserInfo(user)); + dispatch(setSuccess(true)); + router.push('/Home'); + }; + + // Handle Google redirect + useEffect(() => { + const handleGoogleRedirect = async () => { + if (router.query.success === 'google') { + setLoading(true); + try { + // Retrieve the access_token cookie using js-cookie + const token = Cookies.get('access_token'); + if (!token) { + throw new Error('No access_token cookie found'); + } + + // Store the token in localStorage as 'token' + localStorage.setItem('token', token); + const decoded = jwt_decode(token); + + // Fetch user details using the token + const response = await retryWithDelay(() => + getUserDetails(decoded._id, token), + ); + const user = response.users[0]; + + await setupUserSession(user); + } catch (error) { + dispatch(setSuccess(false)); + setLoading(false); + throw error; + } + } + }; + + handleGoogleRedirect(); + }, [router, dispatch]); + + if (loading) { + return ; + } + // Handlers const handleModal = useCallback(() => { setOpen((prev) => !prev); diff --git a/src/platform/yarn.lock b/src/platform/yarn.lock index bdc1a1702d..2c499525e8 100644 --- a/src/platform/yarn.lock +++ b/src/platform/yarn.lock @@ -5252,6 +5252,11 @@ js-cookie@^2.2.1: resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" integrity sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ== +js-cookie@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc" + integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" From dae166ffb530559644ee8023b1a2a4a77d8920cc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:16:25 +0300 Subject: [PATCH 3/6] Update next platform staging image tag to stage-a69774f5-1740485591 --- k8s/platform/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/platform/values-stage.yaml b/k8s/platform/values-stage.yaml index 6622293549..bc4cc3ee8b 100644 --- a/k8s/platform/values-stage.yaml +++ b/k8s/platform/values-stage.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-stage-next-platform pullPolicy: Always - tag: stage-07e1532b-1740386563 + tag: stage-a69774f5-1740485591 imagePullSecrets: [] nameOverride: '' fullnameOverride: '' From 30c0325f89f39805bfe6a5415e2473711a232036 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:34:10 +0300 Subject: [PATCH 4/6] move redirect check to protected route --- src/platform/src/core/utils/protectedRoute.js | 64 ++++++++++++- src/platform/src/pages/Home/index.jsx | 91 ------------------- 2 files changed, 63 insertions(+), 92 deletions(-) diff --git a/src/platform/src/core/utils/protectedRoute.js b/src/platform/src/core/utils/protectedRoute.js index 7df4e81298..762875342f 100644 --- a/src/platform/src/core/utils/protectedRoute.js +++ b/src/platform/src/core/utils/protectedRoute.js @@ -2,6 +2,17 @@ import React, { useEffect } from 'react'; import { useRouter } from 'next/router'; import { useSelector, useDispatch } from 'react-redux'; import LogoutUser from '@/core/utils/LogoutUser'; +import Cookies from 'js-cookie'; +import jwt_decode from 'jwt-decode'; +import { + setUserInfo, + setSuccess, +} from '@/lib/store/services/account/LoginSlice'; +import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; +import { getUserDetails } from '@/core/apis/Account'; + +const MAX_RETRIES = 3; +const RETRY_DELAY = 1000; export default function withAuth(Component) { return function WithAuthComponent(props) { @@ -9,10 +20,61 @@ export default function withAuth(Component) { const router = useRouter(); const userCredentials = useSelector((state) => state.login); + const retryWithDelay = async (fn, retries = MAX_RETRIES) => { + try { + return await fn(); + } catch (error) { + if (retries > 0 && error.response?.status === 429) { + await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); + return retryWithDelay(fn, retries - 1); + } + throw error; + } + }; + + const setupUserSession = async (user) => { + if (!user.groups[0]?.grp_title) { + throw new Error( + 'Server error. Contact support to add you to the AirQo Organisation', + ); + } + + localStorage.setItem('loggedUser', JSON.stringify(user)); + + const preferencesResponse = await retryWithDelay(() => + dispatch(getIndividualUserPreferences({ identifier: user._id })), + ); + if (preferencesResponse.payload.success) { + const preferences = preferencesResponse.payload.preferences; + const activeGroup = preferences[0]?.group_id + ? user.groups.find((group) => group._id === preferences[0].group_id) + : user.groups.find((group) => group.grp_title === 'airqo'); + localStorage.setItem('activeGroup', JSON.stringify(activeGroup)); + } + + dispatch(setUserInfo(user)); + dispatch(setSuccess(true)); + }; + useEffect(() => { if (typeof window !== 'undefined') { const storedUserGroup = localStorage.getItem('activeGroup'); + // Handle Google redirect first + if (router.query.success === 'google') { + const token = Cookies.get('access_token'); + if (token) { + localStorage.setItem('token', token); + const decoded = jwt_decode(token); + retryWithDelay(() => getUserDetails(decoded._id, token)) + .then((response) => setupUserSession(response.users[0])) + .catch((error) => { + console.error('Google auth error:', error); + router.push('/account/login'); + }); + } + } + if (!userCredentials.success) { router.push('/account/login'); } @@ -21,7 +83,7 @@ export default function withAuth(Component) { LogoutUser(dispatch, router); } } - }, [userCredentials, dispatch, router]); + }, [userCredentials, dispatch, router, retryWithDelay]); // Render the component if the user is authenticated return userCredentials.success ? : null; diff --git a/src/platform/src/pages/Home/index.jsx b/src/platform/src/pages/Home/index.jsx index ef9e0c2fe4..d2457eb2cf 100644 --- a/src/platform/src/pages/Home/index.jsx +++ b/src/platform/src/pages/Home/index.jsx @@ -17,18 +17,6 @@ import { import HomeSkeleton from '@/components/skeletons/HomeSkeleton'; import CustomModal from '@/components/Modal/videoModals/CustomModal'; import StepProgress from '@/components/steppers/CircularStepper'; -import Cookies from 'js-cookie'; -import jwt_decode from 'jwt-decode'; -import { useRouter } from 'next/router'; -import { - setUserInfo, - setSuccess, -} from '@/lib/store/services/account/LoginSlice'; -import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; -import { getUserDetails } from '@/core/apis/Account'; - -const MAX_RETRIES = 3; -const RETRY_DELAY = 1000; // Video URL constant const ANALYTICS_VIDEO_URL = @@ -72,7 +60,6 @@ const createSteps = (handleModal, handleCardClick) => [ const Home = () => { const dispatch = useDispatch(); - const router = useRouter(); // Selectors const checkListData = useSelector((state) => state.checklists.checklist); @@ -80,8 +67,6 @@ const Home = () => { const checkListStatus = useSelector((state) => state.checklists.status); // State hooks - const [loading, setLoading] = useState(false); - const [open, setOpen] = useState(false); const [step, setStep] = useState(0); const totalSteps = 4; @@ -100,82 +85,6 @@ const Home = () => { return null; }, []); - const retryWithDelay = async (fn, retries = MAX_RETRIES) => { - try { - return await fn(); - } catch (error) { - if (retries > 0 && error.response?.status === 429) { - await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY)); - return retryWithDelay(fn, retries - 1); - } - throw error; - } - }; - - // Refactored session setup logic for reuse - const setupUserSession = async (user) => { - if (!user.groups[0]?.grp_title) { - throw new Error( - 'Server error. Contact support to add you to the AirQo Organisation', - ); - } - - localStorage.setItem('loggedUser', JSON.stringify(user)); - - const preferencesResponse = await retryWithDelay(() => - dispatch(getIndividualUserPreferences({ identifier: user._id })), - ); - if (preferencesResponse.payload.success) { - const preferences = preferencesResponse.payload.preferences; - const activeGroup = preferences[0]?.group_id - ? user.groups.find((group) => group._id === preferences[0].group_id) - : user.groups.find((group) => group.grp_title === 'airqo'); - localStorage.setItem('activeGroup', JSON.stringify(activeGroup)); - } - - dispatch(setUserInfo(user)); - dispatch(setSuccess(true)); - router.push('/Home'); - }; - - // Handle Google redirect - useEffect(() => { - const handleGoogleRedirect = async () => { - if (router.query.success === 'google') { - setLoading(true); - try { - // Retrieve the access_token cookie using js-cookie - const token = Cookies.get('access_token'); - if (!token) { - throw new Error('No access_token cookie found'); - } - - // Store the token in localStorage as 'token' - localStorage.setItem('token', token); - const decoded = jwt_decode(token); - - // Fetch user details using the token - const response = await retryWithDelay(() => - getUserDetails(decoded._id, token), - ); - const user = response.users[0]; - - await setupUserSession(user); - } catch (error) { - dispatch(setSuccess(false)); - setLoading(false); - throw error; - } - } - }; - - handleGoogleRedirect(); - }, [router, dispatch]); - - if (loading) { - return ; - } - // Handlers const handleModal = useCallback(() => { setOpen((prev) => !prev); From 41c77547f2bf0b407496e0ad41b97a274327ca34 Mon Sep 17 00:00:00 2001 From: Belinda Marion Kobusingye <46527380+Codebmk@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:49:53 +0300 Subject: [PATCH 5/6] block render till redirect is handled --- src/platform/src/core/utils/protectedRoute.js | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/platform/src/core/utils/protectedRoute.js b/src/platform/src/core/utils/protectedRoute.js index 762875342f..cc2bbf2469 100644 --- a/src/platform/src/core/utils/protectedRoute.js +++ b/src/platform/src/core/utils/protectedRoute.js @@ -10,6 +10,7 @@ import { } from '@/lib/store/services/account/LoginSlice'; import { getIndividualUserPreferences } from '@/lib/store/services/account/UserDefaultsSlice'; import { getUserDetails } from '@/core/apis/Account'; +import Spinner from '../../common/components/Spinner'; const MAX_RETRIES = 3; const RETRY_DELAY = 1000; @@ -19,6 +20,9 @@ export default function withAuth(Component) { const dispatch = useDispatch(); const router = useRouter(); const userCredentials = useSelector((state) => state.login); + const [isRedirecting, setIsRedirecting] = React.useState( + router.query.success === 'google', + ); const retryWithDelay = async (fn, retries = MAX_RETRIES) => { try { @@ -58,8 +62,6 @@ export default function withAuth(Component) { useEffect(() => { if (typeof window !== 'undefined') { - const storedUserGroup = localStorage.getItem('activeGroup'); - // Handle Google redirect first if (router.query.success === 'google') { const token = Cookies.get('access_token'); @@ -70,11 +72,17 @@ export default function withAuth(Component) { .then((response) => setupUserSession(response.users[0])) .catch((error) => { console.error('Google auth error:', error); + setIsRedirecting(false); router.push('/account/login'); }); + } else { + setIsRedirecting(false); + router.push('/account/login'); } + return; // Exit early to prevent further checks until redirect is resolved } + const storedUserGroup = localStorage.getItem('activeGroup'); if (!userCredentials.success) { router.push('/account/login'); } @@ -83,7 +91,12 @@ export default function withAuth(Component) { LogoutUser(dispatch, router); } } - }, [userCredentials, dispatch, router, retryWithDelay]); + }, [userCredentials, dispatch, router, retryWithDelay, isRedirecting]); + + // Block rendering until redirect is handled + if (isRedirecting) { + return ; + } // Render the component if the user is authenticated return userCredentials.success ? : null; From 716b56b9c8aa8908bcddb38317ef94c412af862f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:59:48 +0300 Subject: [PATCH 6/6] Update next platform staging image tag to stage-963fc29b-1740488187 --- k8s/platform/values-stage.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8s/platform/values-stage.yaml b/k8s/platform/values-stage.yaml index bc4cc3ee8b..435920b26e 100644 --- a/k8s/platform/values-stage.yaml +++ b/k8s/platform/values-stage.yaml @@ -2,7 +2,7 @@ replicaCount: 1 image: repository: eu.gcr.io/airqo-250220/airqo-stage-next-platform pullPolicy: Always - tag: stage-a69774f5-1740485591 + tag: stage-963fc29b-1740488187 imagePullSecrets: [] nameOverride: '' fullnameOverride: ''