Merge pull request #21 from rkheftan/chore/google-authentication
Chore/google authentication
This commit is contained in:
2
.env
2
.env
@@ -1,4 +1,4 @@
|
|||||||
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com/
|
VITE_GOOGLE_CLIENT_ID=https://272098283932-bft2gvlgjn8edopg0lnqjq1i9ekdmipt.apps.googleusercontent.com
|
||||||
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
|
VITE_DEFUALT_AUTH_RETURN_URL=/setting/profile
|
||||||
VITE_API_URL=https://accounts.business-harmony.com/api/
|
VITE_API_URL=https://accounts.business-harmony.com/api/
|
||||||
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
|
VITE_IDENTITY_URL=https://accounts.business-harmony.com/connect/token
|
||||||
|
|||||||
10
package-lock.json
generated
10
package-lock.json
generated
@@ -24,6 +24,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
@@ -4344,6 +4345,15 @@
|
|||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jwt-decode": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/keyv": {
|
"node_modules/keyv": {
|
||||||
"version": "4.5.4",
|
"version": "4.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
"i18next-browser-languagedetector": "^8.2.0",
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
"i18next-http-backend": "^3.0.2",
|
"i18next-http-backend": "^3.0.2",
|
||||||
"iconsax-react": "^0.0.8",
|
"iconsax-react": "^0.0.8",
|
||||||
|
"jwt-decode": "^4.0.0",
|
||||||
"libphonenumber-js": "^1.12.10",
|
"libphonenumber-js": "^1.12.10",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
|
|||||||
22
src/App.tsx
22
src/App.tsx
@@ -3,15 +3,23 @@ import './App.css';
|
|||||||
import { LanguageManager } from '@/components/LanguageManager';
|
import { LanguageManager } from '@/components/LanguageManager';
|
||||||
import { RouterProvider } from 'react-router-dom';
|
import { RouterProvider } from 'react-router-dom';
|
||||||
import { router } from '@/routes';
|
import { router } from '@/routes';
|
||||||
|
import { useAuth } from './hooks/useAuth';
|
||||||
|
import { Loading } from './components/Loading';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
const { authFinished } = useAuth();
|
||||||
<>
|
|
||||||
<CssBaseline />
|
if (authFinished) {
|
||||||
<LanguageManager />
|
return (
|
||||||
<RouterProvider router={router} />
|
<>
|
||||||
</>
|
<CssBaseline />
|
||||||
);
|
<LanguageManager />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Loading />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Box, IconButton, Skeleton, Typography } from '@mui/material';
|
import { Box, IconButton, Skeleton, Typography } from '@mui/material';
|
||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { More } from 'iconsax-react';
|
import { More } from 'iconsax-react';
|
||||||
import type { User } from './type';
|
import type { UserInfo } from '@/contexts/AuthContext';
|
||||||
|
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
user: User;
|
user: UserInfo;
|
||||||
loading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
|
export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
|
||||||
@@ -20,15 +19,9 @@ export const Header: React.FC<HeaderProps> = ({ user, loading }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">{user.fullName}</Typography>
|
||||||
{loading ? (
|
|
||||||
<Skeleton variant="text" />
|
|
||||||
) : (
|
|
||||||
user.firstName + ' ' + user.lastName
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
<Typography variant="body2" color="textSecondary">
|
||||||
{loading ? <Skeleton variant="text" /> : user.phoneNumber}
|
{user.phoneNumber ?? user.email}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { Box, useMediaQuery, useTheme } from '@mui/material';
|
|||||||
import { Header } from './Header';
|
import { Header } from './Header';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Toolbar } from './Toolbar';
|
import { Toolbar } from './Toolbar';
|
||||||
import { useApi } from '@/hooks/useApi';
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
import { fetchProfile } from '@/features/profile/api/settingsApi';
|
|
||||||
import type { User } from './type';
|
|
||||||
|
|
||||||
export const Layout = () => {
|
export const Layout = () => {
|
||||||
const navItemConfigs = buildNavItems(appRoutes);
|
const navItemConfigs = buildNavItems(appRoutes);
|
||||||
@@ -16,7 +14,7 @@ export const Layout = () => {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
const isMobile = useMediaQuery(theme.breakpoints.down('md'));
|
||||||
const [sideNavOpen, setSideNavOpen] = useState(false);
|
const [sideNavOpen, setSideNavOpen] = useState(false);
|
||||||
const { data, loading } = useApi(fetchProfile, { immediate: true });
|
const { userInfo } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -44,13 +42,7 @@ export const Layout = () => {
|
|||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
sideNavOpen={sideNavOpen}
|
sideNavOpen={sideNavOpen}
|
||||||
setSideNavOpen={setSideNavOpen}
|
setSideNavOpen={setSideNavOpen}
|
||||||
loading={loading}
|
user={userInfo}
|
||||||
user={{
|
|
||||||
firstName: data?.firstName || '',
|
|
||||||
lastName: data?.lastName || '',
|
|
||||||
phoneNumber: data?.phoneNumber || '',
|
|
||||||
profileUrl: data?.profileImageUrl,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
<Box sx={{ flex: 1, overflowY: 'auto' }}>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
@@ -60,32 +52,8 @@ export const Layout = () => {
|
|||||||
<SideNav
|
<SideNav
|
||||||
open={sideNavOpen}
|
open={sideNavOpen}
|
||||||
onClose={() => setSideNavOpen(false)}
|
onClose={() => setSideNavOpen(false)}
|
||||||
header={
|
header={isMobile ? undefined : <Header user={userInfo} />}
|
||||||
isMobile ? undefined : (
|
footer={isMobile ? <Header user={userInfo} /> : undefined}
|
||||||
<Header
|
|
||||||
loading={loading}
|
|
||||||
user={{
|
|
||||||
firstName: data?.firstName || '',
|
|
||||||
lastName: data?.lastName || '',
|
|
||||||
phoneNumber: data?.phoneNumber || '',
|
|
||||||
profileUrl: data?.profileImageUrl,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
footer={
|
|
||||||
isMobile ? (
|
|
||||||
<Header
|
|
||||||
loading={loading}
|
|
||||||
user={{
|
|
||||||
firstName: data?.firstName || '',
|
|
||||||
lastName: data?.lastName || '',
|
|
||||||
phoneNumber: data?.phoneNumber || '',
|
|
||||||
profileUrl: data?.profileImageUrl,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : undefined
|
|
||||||
}
|
|
||||||
navConfig={navItemConfigs}
|
navConfig={navItemConfigs}
|
||||||
activePath={location.pathname + location.hash}
|
activePath={location.pathname + location.hash}
|
||||||
selectedVariant="textOnly"
|
selectedVariant="textOnly"
|
||||||
|
|||||||
@@ -8,15 +8,13 @@ import {
|
|||||||
import { Icon } from '@rkheftan/harmony-ui';
|
import { Icon } from '@rkheftan/harmony-ui';
|
||||||
import { HambergerMenu, Menu } from 'iconsax-react';
|
import { HambergerMenu, Menu } from 'iconsax-react';
|
||||||
import type { Dispatch, SetStateAction } from 'react';
|
import type { Dispatch, SetStateAction } from 'react';
|
||||||
import type { User } from './type';
|
import type { UserInfo } from '@/contexts/AuthContext';
|
||||||
import Logo from '../Logo';
|
|
||||||
|
|
||||||
interface ToolbarProps {
|
interface ToolbarProps {
|
||||||
sideNavOpen: boolean;
|
sideNavOpen: boolean;
|
||||||
setSideNavOpen: Dispatch<SetStateAction<boolean>>;
|
setSideNavOpen: Dispatch<SetStateAction<boolean>>;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
user: User;
|
user: UserInfo;
|
||||||
loading: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Toolbar: React.FC<ToolbarProps> = ({
|
export const Toolbar: React.FC<ToolbarProps> = ({
|
||||||
@@ -60,17 +58,14 @@ export const Toolbar: React.FC<ToolbarProps> = ({
|
|||||||
<Box
|
<Box
|
||||||
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 1 }}
|
sx={{ display: 'flex', height: '100%', alignItems: 'center', gap: 1 }}
|
||||||
>
|
>
|
||||||
{isMobile &&
|
{isMobile && (
|
||||||
(loading ? (
|
<Avatar
|
||||||
<Skeleton variant="circular" />
|
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
||||||
) : (
|
src={user.picture}
|
||||||
<Avatar
|
>
|
||||||
sx={{ width: 32, height: 32, fontSize: '14px' }}
|
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
||||||
src={user.profileUrl}
|
</Avatar>
|
||||||
>
|
)}
|
||||||
{user.firstName.charAt(0) + ' ' + user.lastName.charAt(0)}
|
|
||||||
</Avatar>
|
|
||||||
))}
|
|
||||||
<IconButton>
|
<IconButton>
|
||||||
<Icon Component={Menu} variant="Bold" />
|
<Icon Component={Menu} variant="Bold" />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
// TODO: this type file is temporary and should replace it with the actual use type and value when api is ready
|
|
||||||
|
|
||||||
export interface User {
|
|
||||||
firstName: string;
|
|
||||||
lastName: string;
|
|
||||||
phoneNumber: string;
|
|
||||||
profileUrl?: string;
|
|
||||||
}
|
|
||||||
|
|||||||
19
src/components/Loading.tsx
Normal file
19
src/components/Loading.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { CircularProgress, Stack } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export const Loading = () => {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
backgroundColor: (t) => t.palette.background.default,
|
||||||
|
position: 'fixed',
|
||||||
|
inset: '0',
|
||||||
|
zIndex: (t) => t.zIndex.tooltip + 1,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CircularProgress size={50} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,5 +1,16 @@
|
|||||||
|
import type { GUID } from '@/types/commonTypes';
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
picture: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
fullName: string;
|
||||||
|
email: string;
|
||||||
|
phoneNumber: string;
|
||||||
|
userID: GUID;
|
||||||
|
}
|
||||||
|
|
||||||
type AuthContextType = {
|
type AuthContextType = {
|
||||||
accessToken: string | null;
|
accessToken: string | null;
|
||||||
getToken: () => string | null;
|
getToken: () => string | null;
|
||||||
@@ -9,6 +20,8 @@ type AuthContextType = {
|
|||||||
expires_in: number;
|
expires_in: number;
|
||||||
}) => void;
|
}) => void;
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
|
authFinished: boolean;
|
||||||
|
userInfo: UserInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContextType | undefined>(
|
export const AuthContext = createContext<AuthContextType | undefined>(
|
||||||
|
|||||||
@@ -59,3 +59,25 @@ export const generateTokenWithOtp = (request: GenerateTokenWithOTP) => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface GenerateTokenWithGoogle {
|
||||||
|
idToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const generateTokenWithGoogle = (request: GenerateTokenWithGoogle) => {
|
||||||
|
const body = new URLSearchParams();
|
||||||
|
body.set('grant_type', 'google');
|
||||||
|
body.set('client_id', import.meta.env.VITE_IDENTITY_CLIENT_ID);
|
||||||
|
body.set('scope', import.meta.env.VITE_IDENTITY_SCOPE);
|
||||||
|
body.set('idtoken', request.idToken);
|
||||||
|
|
||||||
|
return apiClient.post<GenerateTokenResponse>(
|
||||||
|
import.meta.env.VITE_IDENTITY_URL,
|
||||||
|
body.toString(),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import type { GoogleCodeClientResponse } from '../../types/userTypes';
|
import type {
|
||||||
|
GoogleCodeClientResponse,
|
||||||
|
LoginOrSignUpWithGoogleRequest,
|
||||||
|
} from '../../types/userTypes';
|
||||||
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
|
import { loginOrSignUpWithGoogle } from '../../api/authorizationAPI';
|
||||||
import type { GUID } from '@/types/commonTypes';
|
import type { GUID } from '@/types/commonTypes';
|
||||||
import { Google } from 'iconsax-react';
|
import { Google } from 'iconsax-react';
|
||||||
import { Icon, useToast } from '@rkheftan/harmony-ui';
|
import { Icon, useToast } from '@rkheftan/harmony-ui';
|
||||||
import { useApi } from '@/hooks/useApi';
|
import { useApi } from '@/hooks/useApi';
|
||||||
|
import { generateTokenWithGoogle } from '../../api/identityAPI';
|
||||||
|
import { useAuth } from '@/hooks/useAuth';
|
||||||
|
|
||||||
export interface GoogleAuthenticationProps {
|
export interface GoogleAuthenticationProps {
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
@@ -24,6 +29,7 @@ export const GoogleAuthentication = ({
|
|||||||
useApi(loginOrSignUpWithGoogle);
|
useApi(loginOrSignUpWithGoogle);
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const clientRef = useRef<any>(null);
|
const clientRef = useRef<any>(null);
|
||||||
|
const auth = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
@@ -37,16 +43,22 @@ export const GoogleAuthentication = ({
|
|||||||
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
|
client_id: import.meta.env.VITE_GOOGLE_CLIENT_ID,
|
||||||
scope: 'openid email profile',
|
scope: 'openid email profile',
|
||||||
ux_mode: 'popup',
|
ux_mode: 'popup',
|
||||||
response_type: 'id_token',
|
response_type: 'code',
|
||||||
callback: async (resp: GoogleCodeClientResponse) => {
|
callback: async (resp: GoogleCodeClientResponse) => {
|
||||||
const res = await loginWithGoogleCall({
|
const apiRequest: LoginOrSignUpWithGoogleRequest = {
|
||||||
idToken: resp.id_token,
|
idToken: resp.id_token,
|
||||||
returnUrl: authReturnUrl,
|
returnUrl: authReturnUrl,
|
||||||
});
|
};
|
||||||
|
const res = await loginWithGoogleCall(apiRequest);
|
||||||
|
|
||||||
if (!res) return;
|
if (!res) return;
|
||||||
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
|
const tokenRes = await generateTokenWithGoogle(apiRequest);
|
||||||
|
auth.login({
|
||||||
|
...tokenRes.data,
|
||||||
|
});
|
||||||
|
|
||||||
onGoogleAuthenticated(res.userId);
|
onGoogleAuthenticated(res.userId);
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
|
|||||||
@@ -1,8 +1,31 @@
|
|||||||
// useAuth.tsx
|
// useAuth.tsx
|
||||||
import { AuthContext } from '@/contexts/AuthContext';
|
import { AuthContext, type UserInfo } from '@/contexts/AuthContext';
|
||||||
import type { GenerateTokenResponse } from '@/features/authorization/api/identityAPI';
|
import type { GenerateTokenResponse } from '@/features/authorization/api/identityAPI';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { useEffect, useState, type ReactNode } from 'react';
|
import { useEffect, useState, type ReactNode } from 'react';
|
||||||
|
import { jwtDecode } from 'jwt-decode';
|
||||||
|
import type { GUID } from '@/types/commonTypes';
|
||||||
|
|
||||||
|
export interface AccessTokenJwtPayload {
|
||||||
|
iss: string;
|
||||||
|
nbf: number;
|
||||||
|
iat: number;
|
||||||
|
exp: number;
|
||||||
|
scope: string[];
|
||||||
|
amr: string[];
|
||||||
|
client_id: string;
|
||||||
|
sub: GUID;
|
||||||
|
auth_time: number;
|
||||||
|
idp: string;
|
||||||
|
picture: string;
|
||||||
|
given_name: string;
|
||||||
|
family_name: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
phone_number: string;
|
||||||
|
session: GUID;
|
||||||
|
jti: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
|
export const ACCESS_TOKEN_KEY: 'access_token' = 'access_token' as const;
|
||||||
export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
|
export const REFRESH_TOKEN_KEY: 'refresh_token' = 'refresh_token' as const;
|
||||||
@@ -12,20 +35,31 @@ let inMemoryToken: string | null = null;
|
|||||||
let expiresAt = 0;
|
let expiresAt = 0;
|
||||||
|
|
||||||
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
|
||||||
|
const [authFinished, setAuthFinished] = useState<boolean>(false);
|
||||||
const [accessToken, setAccessToken] = useState<string | null>(
|
const [accessToken, setAccessToken] = useState<string | null>(
|
||||||
sessionStorage.getItem(ACCESS_TOKEN_KEY),
|
sessionStorage.getItem(ACCESS_TOKEN_KEY),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize from sessionStorage (page reload)
|
// Initialize from sessionStorage (page reload)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
|
const handleAndSetToken = async () => {
|
||||||
const exp = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0);
|
const token = sessionStorage.getItem(ACCESS_TOKEN_KEY);
|
||||||
|
|
||||||
if (token && Date.now() < exp) {
|
if (token) {
|
||||||
inMemoryToken = token;
|
setUserInfoFromToken(token);
|
||||||
expiresAt = exp;
|
await refreshAccessToken();
|
||||||
setAccessToken(token);
|
inMemoryToken = token;
|
||||||
}
|
expiresAt = Number(sessionStorage.getItem(EXPIRES_IN_KEY) || 0);
|
||||||
|
setAccessToken(token);
|
||||||
|
|
||||||
|
setAuthFinished(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAuthFinished(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
handleAndSetToken();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Background refresh
|
// Background refresh
|
||||||
@@ -38,8 +72,6 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
setAccessToken(inMemoryToken);
|
setAccessToken(inMemoryToken);
|
||||||
};
|
};
|
||||||
|
|
||||||
handleRefreshTokenIfNeeded();
|
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
await handleRefreshTokenIfNeeded();
|
await handleRefreshTokenIfNeeded();
|
||||||
}, 30_000);
|
}, 30_000);
|
||||||
@@ -52,6 +84,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
refresh_token: string;
|
refresh_token: string;
|
||||||
expires_in: number;
|
expires_in: number;
|
||||||
}) {
|
}) {
|
||||||
|
setUserInfoFromToken(tokens.access_token);
|
||||||
inMemoryToken = tokens.access_token;
|
inMemoryToken = tokens.access_token;
|
||||||
expiresAt = Date.now() + tokens.expires_in * 1000;
|
expiresAt = Date.now() + tokens.expires_in * 1000;
|
||||||
|
|
||||||
@@ -100,6 +133,7 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
expiresAt = Date.now() + result.data.expires_in * 1000;
|
expiresAt = Date.now() + result.data.expires_in * 1000;
|
||||||
|
|
||||||
sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string);
|
sessionStorage.setItem(ACCESS_TOKEN_KEY, inMemoryToken as string);
|
||||||
|
sessionStorage.setItem(REFRESH_TOKEN_KEY, result.data.refresh_token);
|
||||||
sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
|
sessionStorage.setItem(EXPIRES_IN_KEY, String(expiresAt));
|
||||||
|
|
||||||
setAccessToken(inMemoryToken);
|
setAccessToken(inMemoryToken);
|
||||||
@@ -111,8 +145,33 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setUserInfoFromToken(token: string) {
|
||||||
|
const accessTokenPayload = jwtDecode<AccessTokenJwtPayload>(token);
|
||||||
|
|
||||||
|
const userInfo: UserInfo = {
|
||||||
|
picture: accessTokenPayload.picture,
|
||||||
|
firstName: accessTokenPayload.given_name,
|
||||||
|
lastName: accessTokenPayload.family_name,
|
||||||
|
fullName: accessTokenPayload.name,
|
||||||
|
email: accessTokenPayload.email,
|
||||||
|
phoneNumber: accessTokenPayload.phone_number,
|
||||||
|
userID: accessTokenPayload.sub,
|
||||||
|
};
|
||||||
|
|
||||||
|
setUserInfo(userInfo);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={{ accessToken, getToken, login, logout }}>
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
accessToken,
|
||||||
|
getToken,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
authFinished,
|
||||||
|
userInfo: userInfo as UserInfo,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Suspense, type ReactNode } from 'react';
|
|||||||
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
import { createBrowserRouter, type RouteObject } from 'react-router-dom';
|
||||||
import { appRoutes, type RouteConfig } from './config';
|
import { appRoutes, type RouteConfig } from './config';
|
||||||
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
import { ProtectedRoute } from '@/components/ProtectedRoute';
|
||||||
|
import { Loading } from '@/components/Loading';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A recursive function to map our custom route config to the format
|
* A recursive function to map our custom route config to the format
|
||||||
@@ -11,7 +12,7 @@ function mapRoutes(routes: RouteConfig[]): RouteObject[] {
|
|||||||
return routes.map((route) => {
|
return routes.map((route) => {
|
||||||
// Start with the base element, wrapped in Suspense for lazy loading
|
// Start with the base element, wrapped in Suspense for lazy loading
|
||||||
let element: ReactNode = (
|
let element: ReactNode = (
|
||||||
<Suspense fallback={<div>Loading...</div>}>{route.element}</Suspense>
|
<Suspense fallback={<Loading />}>{route.element}</Suspense>
|
||||||
);
|
);
|
||||||
|
|
||||||
// Conditionally wrap the element in the specified layout
|
// Conditionally wrap the element in the specified layout
|
||||||
|
|||||||
Reference in New Issue
Block a user