[FEAT] review the loggin model to be satble and efficient with user interactions
Signed-off-by: Edouard DUPIN <yui.heero@gmail.com>
This commit is contained in:
parent
83f8ec0e9b
commit
4caec9a54a
@ -35,6 +35,7 @@ public class WebLauncherLocal extends WebLauncher {
|
||||
ConfigBaseVariable.dbPort = "3906";
|
||||
ConfigBaseVariable.testMode = "true";
|
||||
}
|
||||
// Test fail of SSO: ConfigBaseVariable.ssoAdress = null;
|
||||
try {
|
||||
super.migrateDB();
|
||||
} catch (final Exception e) {
|
||||
|
@ -1,6 +1,13 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Box, Flex, IconButton, SliderTrack, Text } from '@chakra-ui/react';
|
||||
import {
|
||||
Box,
|
||||
Flex,
|
||||
IconButton,
|
||||
SliderTrack,
|
||||
Text,
|
||||
chakra,
|
||||
} from '@chakra-ui/react';
|
||||
import {
|
||||
MdFastForward,
|
||||
MdFastRewind,
|
||||
@ -223,6 +230,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
|
||||
paddingX="10px"
|
||||
marginX="15px"
|
||||
bottom={0}
|
||||
//top="calc(100% - 150px)"
|
||||
left={0}
|
||||
right={0}
|
||||
zIndex={1000}
|
||||
@ -318,7 +326,9 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
|
||||
marginLeft="auto"
|
||||
variant="ghost"
|
||||
>
|
||||
<MdNavigateBefore style={{ width: '100%', height: '100%' }} />{' '}
|
||||
<MdNavigateBefore
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
/>{' '}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
{...configButton}
|
||||
@ -357,7 +367,7 @@ export const AudioPlayer = ({}: AudioPlayerProps) => {
|
||||
</Flex>
|
||||
)}
|
||||
|
||||
<audio
|
||||
<chakra.audio
|
||||
src={mediaSource}
|
||||
ref={audioRef}
|
||||
//preload={true}
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
MdMore,
|
||||
MdOutlinePlaylistPlay,
|
||||
MdOutlineUploadFile,
|
||||
MdRestartAlt,
|
||||
MdSupervisedUserCircle,
|
||||
} from 'react-icons/md';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@ -47,7 +48,6 @@ import {
|
||||
MenuTrigger,
|
||||
} from '@/components/ui/menu';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { SessionState } from '@/service/SessionState';
|
||||
import { useSessionService } from '@/service/session';
|
||||
import { colors } from '@/theme/colors';
|
||||
import {
|
||||
@ -120,11 +120,16 @@ export const TopBar = ({
|
||||
const navigate = useNavigate();
|
||||
const { colorMode, toggleColorMode } = useColorMode();
|
||||
const { session } = useServiceContext();
|
||||
const { clearToken } = useSessionService();
|
||||
const { clearToken, isConnected } = useSessionService();
|
||||
const backColor = useColorModeValue('back.100', 'back.800');
|
||||
const drawerDisclose = useDisclosure();
|
||||
const onChangeTheme = () => {
|
||||
drawerDisclose.onOpen();
|
||||
const isVisible = useBreakpointValue({ base: false, md: true });
|
||||
const onOpenLeftMenu = () => {
|
||||
if (!isConnected) {
|
||||
onForceReload();
|
||||
} else {
|
||||
drawerDisclose.onOpen();
|
||||
}
|
||||
};
|
||||
const onSignIn = (): void => {
|
||||
clearToken();
|
||||
@ -141,7 +146,10 @@ export const TopBar = ({
|
||||
const onKarso = (): void => {
|
||||
requestOpenSite();
|
||||
};
|
||||
const isVisible = useBreakpointValue({ base: false, md: true });
|
||||
const onForceReload = (): void => {
|
||||
// @ts-expect-error
|
||||
window.location.reload(true);
|
||||
};
|
||||
return (
|
||||
<Flex
|
||||
minWidth="320px"
|
||||
@ -158,7 +166,7 @@ export const TopBar = ({
|
||||
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
||||
zIndex={200}
|
||||
>
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onChangeTheme}>
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onOpenLeftMenu}>
|
||||
<HStack>
|
||||
<LuAlignJustify />
|
||||
{isVisible && (
|
||||
@ -169,7 +177,7 @@ export const TopBar = ({
|
||||
</HStack>
|
||||
</Button>
|
||||
{title && (
|
||||
<Text
|
||||
<Flex
|
||||
truncate
|
||||
fontSize="20px"
|
||||
fontWeight="bold"
|
||||
@ -183,11 +191,11 @@ export const TopBar = ({
|
||||
{titleIcon}
|
||||
{title}
|
||||
</Flex>
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{children}
|
||||
<Flex right="0">
|
||||
{session?.state !== SessionState.CONNECTED && (
|
||||
{!session?.isConnected && (
|
||||
<>
|
||||
<Button {...BUTTON_TOP_BAR_PROPERTY} onClick={onSignIn}>
|
||||
<LuLogIn />
|
||||
@ -207,7 +215,7 @@ export const TopBar = ({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{session?.state === SessionState.CONNECTED && (
|
||||
{session?.isConnected && (
|
||||
<MenuRoot>
|
||||
<MenuTrigger asChild>
|
||||
<IconButton {...BUTTON_TOP_BAR_PROPERTY} width={TOP_BAR_HEIGHT}>
|
||||
@ -248,6 +256,13 @@ export const TopBar = ({
|
||||
<MenuItem value="karso" valueText="Karso" onClick={onKarso}>
|
||||
<LuKeySquare /> Karso (SSO)
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
value="force_reload"
|
||||
valueText="Karso"
|
||||
onClick={onForceReload}
|
||||
>
|
||||
<MdRestartAlt /> force reload
|
||||
</MenuItem>
|
||||
{colorMode === 'light' ? (
|
||||
<MenuItem
|
||||
value="set-dark"
|
||||
@ -269,50 +284,52 @@ export const TopBar = ({
|
||||
</MenuRoot>
|
||||
)}
|
||||
</Flex>
|
||||
<DrawerRoot
|
||||
placement="start"
|
||||
onOpenChange={drawerDisclose.onClose}
|
||||
open={drawerDisclose.open}
|
||||
data-testid="top-bar_drawer-root"
|
||||
>
|
||||
<DrawerContent data-testid="top-bar_drawer-content">
|
||||
<DrawerHeader
|
||||
paddingY="auto"
|
||||
as="button"
|
||||
onClick={drawerDisclose.onClose}
|
||||
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
||||
backgroundColor={backColor}
|
||||
color={useColorModeValue('brand.900', 'brand.50')}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
<HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
|
||||
<LuArrowBigLeft />
|
||||
<Span paddingLeft="3px">Karusic</Span>
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
<DrawerBody paddingX="0px">
|
||||
<Box marginY="3" />
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/"
|
||||
title="Home"
|
||||
icon={<MdHome />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/on-air"
|
||||
title="On air"
|
||||
icon={<MdOutlinePlaylistPlay />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/add"
|
||||
title="Add Media"
|
||||
icon={<MdOutlineUploadFile />}
|
||||
/>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</DrawerRoot>
|
||||
{session?.isConnected && (
|
||||
<DrawerRoot
|
||||
placement="start"
|
||||
onOpenChange={drawerDisclose.onClose}
|
||||
open={drawerDisclose.open}
|
||||
data-testid="top-bar_drawer-root"
|
||||
>
|
||||
<DrawerContent data-testid="top-bar_drawer-content">
|
||||
<DrawerHeader
|
||||
paddingY="auto"
|
||||
as="button"
|
||||
onClick={drawerDisclose.onClose}
|
||||
boxShadow={'0px 2px 4px ' + colors.back[900]}
|
||||
backgroundColor={backColor}
|
||||
color={useColorModeValue('brand.900', 'brand.50')}
|
||||
textTransform="uppercase"
|
||||
>
|
||||
<HStack {...BUTTON_TOP_BAR_PROPERTY} cursor="pointer">
|
||||
<LuArrowBigLeft />
|
||||
<Span paddingLeft="3px">Karusic</Span>
|
||||
</HStack>
|
||||
</DrawerHeader>
|
||||
<DrawerBody paddingX="0px">
|
||||
<Box marginY="3" />
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/"
|
||||
title="Home"
|
||||
icon={<MdHome />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/on-air"
|
||||
title="On air"
|
||||
icon={<MdOutlinePlaylistPlay />}
|
||||
/>
|
||||
<ButtonMenuLeft
|
||||
onClickEnd={drawerDisclose.onClose}
|
||||
dest="/add"
|
||||
title="Add Media"
|
||||
icon={<MdOutlineUploadFile />}
|
||||
/>
|
||||
</DrawerBody>
|
||||
</DrawerContent>
|
||||
</DrawerRoot>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ import { SettingsPage } from './home/SettingsPage';
|
||||
import { OnAirPage } from './onAir/OnAirPage';
|
||||
|
||||
export const AppRoutes = () => {
|
||||
const { isReadable } = useHasRight('user');
|
||||
const { isReadable } = useHasRight('USER');
|
||||
return (
|
||||
<HistoryRouter
|
||||
// @ts-expect-error
|
||||
|
@ -1,48 +1,58 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { Center, Heading, Image, Text } from '@chakra-ui/react';
|
||||
import { Center, Flex, Heading, Image, Text } from '@chakra-ui/react';
|
||||
import { MdFactCheck, MdWarning } from 'react-icons/md';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import avatar_generic from '@/assets/images/avatar_generic.svg';
|
||||
import { Icon } from '@/components';
|
||||
import { PageLayoutInfoCenter } from '@/components/Layout/PageLayoutInfoCenter';
|
||||
import { TopBar } from '@/components/TopBar/TopBar';
|
||||
import { isDevelopmentEnvironment } from '@/environment';
|
||||
import { SessionState } from '@/service/SessionState';
|
||||
import { toaster } from '@/components/ui/toaster';
|
||||
import { useSessionService } from '@/service/session';
|
||||
import { b64_to_utf8 } from '@/utils/sso';
|
||||
|
||||
export const SSOPage = () => {
|
||||
const { data, keepConnected, token } = useParams();
|
||||
console.log(`- data: ${data}`);
|
||||
console.log(`- keepConnected: ${keepConnected}`);
|
||||
console.log(`- token: ${token}`);
|
||||
//const { data, keepConnected, token } = useState<string|undefined>();
|
||||
// console.log(`- data: ${data}`);
|
||||
// console.log(`- keepConnected: ${keepConnected}`);
|
||||
// console.log(`- token: ${token}`);
|
||||
const navigate = useNavigate();
|
||||
const { state, setToken, login, clearToken } = useSessionService();
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
setToken(token);
|
||||
} else {
|
||||
clearToken();
|
||||
const { isConnected, errorSession, setToken, login } = useSessionService();
|
||||
useEffectOnce(() => {
|
||||
if (token === undefined || token === '') {
|
||||
return;
|
||||
}
|
||||
}, [token, setToken, clearToken]);
|
||||
const delay = isDevelopmentEnvironment() ? 2000 : 2000;
|
||||
try {
|
||||
setToken(token);
|
||||
} catch (e) {
|
||||
toaster.create({
|
||||
title: 'Connection Fail',
|
||||
description: `invalid token data model.`,
|
||||
type: 'error',
|
||||
});
|
||||
navigate('/');
|
||||
}
|
||||
});
|
||||
const delay = 1000;
|
||||
useEffect(() => {
|
||||
if (state === SessionState.CONNECTED) {
|
||||
const destination = data ? b64_to_utf8(data) : '/';
|
||||
console.log(`program redirect to: ${destination} (${delay}ms)`);
|
||||
if (isConnected) {
|
||||
toaster.create({
|
||||
title: 'Connection Succeed',
|
||||
description: `Welcome back: ${login}.`,
|
||||
type: 'success',
|
||||
});
|
||||
const destination = data ? b64_to_utf8(data) : '/home';
|
||||
const destinationFinal = destination === '' ? '/home' : destination;
|
||||
console.log(`program redirect to: '${destinationFinal}' (${delay}ms)`);
|
||||
setTimeout(() => {
|
||||
navigate(`/${destination}`);
|
||||
// note: check if the "navigate" is in the dependence of the use effect!!!
|
||||
navigate(`/${destinationFinal}`, { replace: true });
|
||||
}, delay);
|
||||
}
|
||||
}, [state]);
|
||||
/*
|
||||
const [searchParams] = useSearchParams();
|
||||
console.log(`data: ${searchParams.get('data')}`);
|
||||
console.log(`keepConnected: ${searchParams.get('keepConnected')}`);
|
||||
console.log(`token: ${searchParams.get('token')}`);
|
||||
const dataFromParam = useGetCreateActionParams();
|
||||
console.log(`data group: ${JSON.stringify(dataFromParam, null, 2)}`);
|
||||
*/
|
||||
}, [isConnected, login, navigate]);
|
||||
return (
|
||||
<>
|
||||
<TopBar />
|
||||
@ -54,31 +64,53 @@ export const SSOPage = () => {
|
||||
<Center w="full">
|
||||
<Image src={avatar_generic} boxSize="150px" borderRadius="full" />
|
||||
</Center>
|
||||
{token === '__CANCEL__' && (
|
||||
<Text>
|
||||
<b>ERROR: </b> Request cancel of connection !
|
||||
</Text>
|
||||
)}
|
||||
{token === '__FAIL__' && (
|
||||
<Text>
|
||||
<b>ERROR: </b> Connection FAIL !
|
||||
</Text>
|
||||
)}
|
||||
{token === '__LOGOUT__' && (
|
||||
<Text>
|
||||
<b>Dis-connected: </b> Redirect soon!{' '}
|
||||
</Text>
|
||||
)}
|
||||
{!['__LOGOUT__', '__FAIL__', '__CANCEL__'].includes(token ?? '') && (
|
||||
{!isConnected && errorSession !== undefined && (
|
||||
<>
|
||||
<Text>
|
||||
<b>Connected: </b> Redirect soon!
|
||||
</Text>
|
||||
<Text>
|
||||
<b>Welcome back: </b> {login}
|
||||
</Text>
|
||||
<Flex color="red.500" fontSize="25px" gap={2} marginX="auto">
|
||||
<Icon sizeIcon="55px">
|
||||
<MdWarning />
|
||||
</Icon>
|
||||
<Text marginY="auto">
|
||||
<b>ERROR: </b> {errorSession.message}
|
||||
</Text>
|
||||
</Flex>
|
||||
{errorSession.description && (
|
||||
<Text
|
||||
whiteSpace="pre-line"
|
||||
fontSize="25px"
|
||||
gap={2}
|
||||
marginX="auto"
|
||||
>
|
||||
{errorSession.description}
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!isConnected && errorSession === undefined && (
|
||||
<Flex color="red.500" fontSize="25px" gap={2} marginX="auto">
|
||||
<Icon sizeIcon="55px">
|
||||
<MdWarning />
|
||||
</Icon>
|
||||
<Text marginY="auto">
|
||||
<b>ERROR: </b> Not connected !
|
||||
</Text>
|
||||
</Flex>
|
||||
)}
|
||||
{isConnected && (
|
||||
<Flex direction="column">
|
||||
<Flex color="green.500" fontSize="25px" gap={2} marginX="auto">
|
||||
<MdFactCheck />
|
||||
<Text marginY="auto">
|
||||
<b>Connected: </b> Redirect soon!
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex fontSize="25px" gap={2} marginX="auto">
|
||||
<Text marginY="auto">
|
||||
<b>Welcome back: </b> {login}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
</PageLayoutInfoCenter>
|
||||
</>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Album, AlbumResource } from '@/back-api';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
@ -23,14 +23,14 @@ export const useAlbumServiceWrapped = (
|
||||
{
|
||||
restApiName: 'ALBUM',
|
||||
primaryKey: 'id',
|
||||
available: session.token !== undefined,
|
||||
available: session.isConnected,
|
||||
getsCall: () => {
|
||||
return AlbumResource.gets({
|
||||
restConfig: session.getRestConfig(),
|
||||
});
|
||||
},
|
||||
},
|
||||
[session.token]
|
||||
[session.isConnected]
|
||||
);
|
||||
|
||||
return { store };
|
||||
|
@ -23,14 +23,14 @@ export const useArtistServiceWrapped = (
|
||||
{
|
||||
restApiName: 'ARTIST',
|
||||
primaryKey: 'id',
|
||||
available: session.token !== undefined,
|
||||
available: session.isConnected,
|
||||
getsCall: () => {
|
||||
return ArtistResource.gets({
|
||||
restConfig: session.getRestConfig(),
|
||||
});
|
||||
},
|
||||
},
|
||||
[session.token]
|
||||
[session.isConnected]
|
||||
);
|
||||
|
||||
return { store };
|
||||
|
@ -23,14 +23,14 @@ export const useGenderServiceWrapped = (
|
||||
{
|
||||
restApiName: 'GENDER',
|
||||
primaryKey: 'id',
|
||||
available: session.token !== undefined,
|
||||
available: session.isConnected,
|
||||
getsCall: () => {
|
||||
return GenderResource.gets({
|
||||
restConfig: session.getRestConfig(),
|
||||
});
|
||||
},
|
||||
},
|
||||
[session.token]
|
||||
[session.isConnected]
|
||||
);
|
||||
|
||||
return { store };
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { ReactNode, createContext, useContext, useMemo } from 'react';
|
||||
|
||||
import { Album, Artist, Gender, Track } from '@/back-api';
|
||||
import {
|
||||
ActivePlaylistServiceProps,
|
||||
useActivePlaylistServiceWrapped,
|
||||
@ -7,7 +8,6 @@ import {
|
||||
import { AlbumServiceProps, useAlbumServiceWrapped } from '@/service/Album';
|
||||
import { ArtistServiceProps, useArtistServiceWrapped } from '@/service/Artist';
|
||||
import { useGenderServiceWrapped } from '@/service/Gender';
|
||||
import { SessionState } from '@/service/SessionState';
|
||||
import { TrackServiceProps, useTrackServiceWrapped } from '@/service/Track';
|
||||
import {
|
||||
RightPart,
|
||||
@ -15,7 +15,6 @@ import {
|
||||
getRestConfig,
|
||||
useSessionServiceWrapped,
|
||||
} from '@/service/session';
|
||||
import { Album, Artist, Gender, Track } from '@/back-api';
|
||||
import { DataStoreType } from '@/utils/data-store';
|
||||
|
||||
export type ServiceContextType = {
|
||||
@ -27,7 +26,6 @@ export type ServiceContextType = {
|
||||
activePlaylist: ActivePlaylistServiceProps;
|
||||
};
|
||||
|
||||
|
||||
function emptyStore<TYPE>(): DataStoreType<TYPE> {
|
||||
return {
|
||||
data: [] as TYPE[],
|
||||
@ -43,22 +41,29 @@ function emptyStore<TYPE>(): DataStoreType<TYPE> {
|
||||
error: undefined,
|
||||
update: (_request: Promise<TYPE>, _key?: string) => {
|
||||
console.error('!!! WTF !!!');
|
||||
return new Promise((resolve, reject) => { reject("fail") });
|
||||
return new Promise((resolve, reject) => {
|
||||
reject('fail');
|
||||
});
|
||||
},
|
||||
updateRaw: (_data: TYPE, _key?: string) => { },
|
||||
remove: (_id: number | string, _request: Promise<void>, _key?: string): void => {
|
||||
updateRaw: (_data: TYPE, _key?: string) => {},
|
||||
remove: (
|
||||
_id: number | string,
|
||||
_request: Promise<void>,
|
||||
_key?: string
|
||||
): void => {
|
||||
console.error('!!! WTF !!!');
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const ServiceContext = createContext<ServiceContextType>({
|
||||
session: {
|
||||
setToken: (token: string) => { },
|
||||
clearToken: () => { },
|
||||
isConnected: false,
|
||||
errorSession: undefined,
|
||||
setToken: (token: string) => {},
|
||||
clearToken: () => {},
|
||||
hasReadRight: (part: RightPart) => false,
|
||||
hasWriteRight: (part: RightPart) => false,
|
||||
state: SessionState.NO_USER,
|
||||
getRestConfig: getRestConfig,
|
||||
},
|
||||
track: {
|
||||
|
@ -1,7 +0,0 @@
|
||||
export enum SessionState {
|
||||
NO_USER,
|
||||
CONNECTING,
|
||||
CONNECTION_FAIL,
|
||||
CONNECTED,
|
||||
DISCONNECT,
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { Track, TrackResource } from '@/back-api';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
@ -23,14 +23,14 @@ export const useTrackServiceWrapped = (
|
||||
{
|
||||
restApiName: 'TRACK',
|
||||
primaryKey: 'id',
|
||||
available: session.token !== undefined,
|
||||
available: session.isConnected,
|
||||
getsCall: () => {
|
||||
return TrackResource.gets({
|
||||
restConfig: session.getRestConfig(),
|
||||
});
|
||||
},
|
||||
},
|
||||
[session.token]
|
||||
[session.isConnected]
|
||||
);
|
||||
|
||||
return { store };
|
||||
|
@ -1,18 +1,18 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { createListCollection } from '@chakra-ui/react';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
|
||||
import {
|
||||
JwtToken,
|
||||
PartRight,
|
||||
RESTConfig,
|
||||
UserMe,
|
||||
RestErrorResponse,
|
||||
UserResource,
|
||||
setErrorApiGlobalCallback,
|
||||
} from '@/back-api';
|
||||
import { toaster } from '@/components/ui/toaster';
|
||||
import { environment, getApiUrl } from '@/environment';
|
||||
import { useServiceContext } from '@/service/ServiceContext';
|
||||
import { SessionState } from '@/service/SessionState';
|
||||
import { isBrowser } from '@/utils/layout';
|
||||
import { parseToken } from '@/utils/sso';
|
||||
|
||||
@ -37,7 +37,7 @@ export const USERS = {
|
||||
export const getUserToken = () => {
|
||||
return localStorage.getItem(TOKEN_KEY);
|
||||
};
|
||||
export type RightPart = 'admin' | 'user';
|
||||
export type RightPart = 'ADMIN' | 'USER';
|
||||
|
||||
export function getRestConfig(): RESTConfig {
|
||||
return {
|
||||
@ -46,14 +46,48 @@ export function getRestConfig(): RESTConfig {
|
||||
};
|
||||
}
|
||||
|
||||
const getDeltaPrint = (date: Date): string => {
|
||||
const now = new Date();
|
||||
let deltaSeconds = Math.floor((date.getTime() - now.getTime()) / 1000);
|
||||
const isPast = deltaSeconds < 0;
|
||||
|
||||
deltaSeconds = Math.abs(deltaSeconds);
|
||||
|
||||
const days = Math.floor(deltaSeconds / 86400);
|
||||
deltaSeconds %= 86400;
|
||||
|
||||
const hours = Math.floor(deltaSeconds / 3600);
|
||||
deltaSeconds %= 3600;
|
||||
|
||||
const minutes = Math.floor(deltaSeconds / 60);
|
||||
const seconds = deltaSeconds % 60;
|
||||
|
||||
const result = [days > 0 ? `${days}j` : '', `${hours}h${minutes}:${seconds}s`]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return `${result}${isPast ? ' ago' : ''}`;
|
||||
};
|
||||
|
||||
const isInThePast = (date: Date): boolean => {
|
||||
const now = new Date();
|
||||
return date.getTime() < now.getTime();
|
||||
};
|
||||
|
||||
export type SessionErrorType = {
|
||||
message: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type SessionServiceProps = {
|
||||
token?: string;
|
||||
isConnected: boolean;
|
||||
errorSession?: SessionErrorType;
|
||||
setToken: (token: string) => void;
|
||||
clearToken: () => void;
|
||||
login?: string;
|
||||
hasReadRight: (part: RightPart) => boolean;
|
||||
hasWriteRight: (part: RightPart) => boolean;
|
||||
state: SessionState;
|
||||
getRestConfig: () => RESTConfig;
|
||||
};
|
||||
|
||||
@ -63,115 +97,161 @@ export const useSessionService = (): SessionServiceProps => {
|
||||
};
|
||||
|
||||
export const useSessionServiceWrapped = (): SessionServiceProps => {
|
||||
const [token, setToken] = useState<string | undefined>(
|
||||
// Load the token from the system (init)
|
||||
const [tokenStorage, setTokenStorage] = useState<string | undefined>(
|
||||
isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined
|
||||
);
|
||||
const [state, setState] = useState<SessionState>(SessionState.NO_USER);
|
||||
const [config, setConfig] = useState<UserMe | undefined>(undefined);
|
||||
// Token that is ready to use
|
||||
const [tokenStr, setTokenStr] = useState<string>('');
|
||||
const [errorSession, setErrorSession] = useState<
|
||||
SessionErrorType | undefined
|
||||
>(undefined);
|
||||
const [token, setToken] = useState<JwtToken | undefined>(undefined);
|
||||
|
||||
useEffectOnce(() => {
|
||||
setErrorApiGlobalCallback((response: Response) => {
|
||||
if (response.status == 401) {
|
||||
console.error('Detect 401 error ==> remove token');
|
||||
clearToken();
|
||||
}
|
||||
});
|
||||
});
|
||||
const getRestConfigLocal = useCallback((): RESTConfig => {
|
||||
return {
|
||||
server: getApiUrl(),
|
||||
token: tokenStr,
|
||||
};
|
||||
}, [tokenStr]);
|
||||
|
||||
const updateRight = useCallback(() => {
|
||||
console.log('update right...');
|
||||
const updateRight = useEffect(() => {
|
||||
//console.log('Detect a new token...');
|
||||
if (isBrowser) {
|
||||
console.log('Detect a new token...');
|
||||
|
||||
if (token === undefined) {
|
||||
console.log(` ==> No User`);
|
||||
setState(SessionState.NO_USER);
|
||||
setConfig(undefined);
|
||||
//console.log('update internal property');
|
||||
if (tokenStorage === undefined) {
|
||||
//console.log(` ==> No User`);
|
||||
setToken(undefined);
|
||||
setTokenStr('');
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
} else {
|
||||
console.log(' ==> Login ... (try to keep right)');
|
||||
setState(SessionState.CONNECTING);
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
//console.log(' ==> Login ... (try to keep right)');
|
||||
let tokenParsed: JwtToken | undefined = undefined;
|
||||
try {
|
||||
tokenParsed = parseToken(tokenStorage);
|
||||
} catch (e) {
|
||||
setErrorSession({
|
||||
message: 'Fail to parse the token',
|
||||
});
|
||||
return;
|
||||
}
|
||||
//console.log(`get new token: ${JSON.stringify(tokenParsed, null, 2)}`);
|
||||
const exp = new Date(tokenParsed.payload.exp * 1000);
|
||||
if (isInThePast(exp)) {
|
||||
//console.log(`token expired at: exp: ${exp.toISOString()}: delta=${getDeltaPrint(exp)}`);
|
||||
const iat = new Date(tokenParsed.payload.iat * 1000);
|
||||
//console.log(`iat: ${iat.toISOString()}: delta=${getDeltaPrint(iat)}`);
|
||||
setErrorSession({
|
||||
message: 'The inserted token has expired',
|
||||
description: `It expired at ${exp.toISOString()}\nSince: ${getDeltaPrint(exp)}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Validate token on the server:
|
||||
UserResource.getMe({
|
||||
restConfig: getRestConfig(),
|
||||
restConfig: {
|
||||
server: getApiUrl(),
|
||||
token: tokenStorage,
|
||||
},
|
||||
})
|
||||
.then((response: UserMe) => {
|
||||
//console.log(` ==> New right arrived to '${response.login}'`);
|
||||
setState(SessionState.CONNECTED);
|
||||
setConfig(response);
|
||||
.then((_response) => {
|
||||
// if the login work well, then the token does not fail with authentication
|
||||
//console.log('Authentication finished: ...');
|
||||
setTokenStr(tokenStorage);
|
||||
setToken(tokenParsed);
|
||||
localStorage.setItem(TOKEN_KEY, tokenStorage);
|
||||
})
|
||||
.catch((error) => {
|
||||
setState(SessionState.CONNECTION_FAIL);
|
||||
setConfig(undefined);
|
||||
//console.log(` ==> Fail to get right: '${error}'`);
|
||||
.catch((error: RestErrorResponse) => {
|
||||
setErrorSession({
|
||||
message: 'The server reject the token.',
|
||||
description: `name: ${error.name}\nmessage: "${error.message}"`,
|
||||
});
|
||||
setTokenStr('');
|
||||
setToken(undefined);
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [localStorage, parseToken, token]);
|
||||
}, [
|
||||
localStorage,
|
||||
parseToken,
|
||||
setErrorSession,
|
||||
setToken,
|
||||
setTokenStr,
|
||||
tokenStorage,
|
||||
]);
|
||||
|
||||
const setTokenLocal = useCallback(
|
||||
(token?: string) => {
|
||||
if (token ? token.startsWith('__') : false) {
|
||||
token = undefined;
|
||||
}
|
||||
setToken(token);
|
||||
updateRight();
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setTokenStorage(token);
|
||||
setTokenStr('');
|
||||
setToken(undefined);
|
||||
setErrorSession(undefined);
|
||||
},
|
||||
[updateRight, setToken]
|
||||
);
|
||||
const clearToken = useCallback(() => {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
setTokenLocal(undefined);
|
||||
setToken(undefined);
|
||||
updateRight();
|
||||
}, [updateRight, setToken]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrorApiGlobalCallback((response: Response) => {
|
||||
if (response.status == 401) {
|
||||
toaster.create({
|
||||
title: 'API request rejected',
|
||||
description: `API Authentication rejected by server.`,
|
||||
type: 'error',
|
||||
});
|
||||
clearToken();
|
||||
}
|
||||
});
|
||||
}, [clearToken]);
|
||||
|
||||
const hasReadRight = useCallback(
|
||||
(part: RightPart) => {
|
||||
//console.log(`config = ${JSON.stringify(config, null, 2)}`);
|
||||
const right = config?.rights[environment.applName];
|
||||
//console.log(`right = ${JSON.stringify(token?.payload?.right?.[environment.applName], null, 2)}`);
|
||||
const right = token?.payload?.right?.[environment.applName];
|
||||
if (right === undefined) {
|
||||
return false;
|
||||
}
|
||||
return [PartRight.READ, PartRight.READ_WRITE].includes(right[part]);
|
||||
},
|
||||
[config]
|
||||
[token]
|
||||
);
|
||||
const hasWriteRight = useCallback(
|
||||
(part: RightPart) => {
|
||||
const right = config?.rights[environment.applName];
|
||||
const right = token?.payload?.right?.[environment.applName];
|
||||
if (right === undefined) {
|
||||
return false;
|
||||
}
|
||||
return [PartRight.READ, PartRight.READ_WRITE].includes(right[part]);
|
||||
},
|
||||
[config]
|
||||
[token]
|
||||
);
|
||||
|
||||
const getRestConfig = useCallback((): RESTConfig => {
|
||||
return {
|
||||
server: getApiUrl(),
|
||||
token: token ?? '',
|
||||
};
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
updateRight();
|
||||
}, [updateRight]);
|
||||
|
||||
return {
|
||||
token,
|
||||
token: tokenStr,
|
||||
isConnected: token !== undefined,
|
||||
errorSession,
|
||||
setToken: setTokenLocal,
|
||||
clearToken,
|
||||
login: config?.login,
|
||||
login: token?.payload?.login,
|
||||
hasReadRight,
|
||||
hasWriteRight,
|
||||
state,
|
||||
getRestConfig,
|
||||
getRestConfig: getRestConfigLocal,
|
||||
};
|
||||
};
|
||||
|
||||
export const useHasRight = (part: RightPart) => {
|
||||
const { token, hasReadRight, hasWriteRight } = useSessionService();
|
||||
const isReadable = useMemo(() => {
|
||||
console.log(`get is read for: ${part} ==> ${hasReadRight(part)}`);
|
||||
//console.log(`get is read for: ${part} ==> ${hasReadRight(part)}`);
|
||||
return hasReadRight(part);
|
||||
}, [token, hasReadRight, part]);
|
||||
const isWritable = useMemo(() => {
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { RESTConfig } from '@/back-api';
|
||||
import { getApiUrl } from '@/environment';
|
||||
import { getUserToken } from '@/service/session';
|
||||
|
||||
export function getRestConfig(): RESTConfig {
|
||||
return {
|
||||
server: getApiUrl(),
|
||||
token: getUserToken() ?? '',
|
||||
};
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { JwtToken, ZodJwtToken } from '@/back-api';
|
||||
import { environment } from '@/environment';
|
||||
import { getApplicationLocation } from '@/utils/applPath';
|
||||
|
||||
@ -72,19 +73,28 @@ export function unHashLocalData(data: string): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function parseToken(token: string): object {
|
||||
const cut = token.split('.');
|
||||
const decoded = b64_to_utf8(cut[1]);
|
||||
const jsonModel = JSON.parse(decoded);
|
||||
if (jsonModel.right === undefined) {
|
||||
return {};
|
||||
const convertBase64asJson = (data: string) => {
|
||||
try {
|
||||
const jsonBuffer = b64_to_utf8(data);
|
||||
return JSON.parse(jsonBuffer);
|
||||
} catch (e) {
|
||||
console.error(`FAil to convert base64 data: ${e}`);
|
||||
}
|
||||
if (jsonModel.right[environment.applName] === undefined) {
|
||||
return {};
|
||||
}
|
||||
return jsonModel.right[environment.applName];
|
||||
}
|
||||
};
|
||||
|
||||
export function parseToken(token: string): JwtToken {
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
console.error(`Wrong token model ${token}`);
|
||||
throw new Error('Token JWT invalid');
|
||||
}
|
||||
const result = {
|
||||
header: convertBase64asJson(parts[0]),
|
||||
payload: convertBase64asJson(parts[1]),
|
||||
signature: parts[2],
|
||||
};
|
||||
return ZodJwtToken.parse(result);
|
||||
}
|
||||
/**
|
||||
* Request Open SSO Global website
|
||||
*/
|
||||
@ -98,14 +108,14 @@ export function requestSignIn(name?: string): void {
|
||||
console.log(
|
||||
`Request sign-in: '${environment.ssoSignIn}' + '${hashLocalData(name)}'`
|
||||
);
|
||||
window.location.href = environment.ssoSignIn + hashLocalData(name) + "/";
|
||||
window.location.href = environment.ssoSignIn + hashLocalData(name) + '/';
|
||||
}
|
||||
/**
|
||||
* Request SSO Disconnect
|
||||
*/
|
||||
export function requestSignOut(name?: string): void {
|
||||
const url = environment.ssoSignOut + hashLocalData(name);
|
||||
console.log(`Request just to the SSO: ${url}`)
|
||||
console.log(`Request just to the SSO: ${url}`);
|
||||
// unlog from the SSO
|
||||
window.location.href = url;
|
||||
}
|
||||
@ -114,7 +124,7 @@ export function requestSignOut(name?: string): void {
|
||||
*/
|
||||
export function requestSignUp(name?: string): void {
|
||||
const url = environment.ssoSignUp + hashLocalData(name);
|
||||
console.log(`Request just to the SSO: ${url}`)
|
||||
console.log(`Request just to the SSO: ${url}`);
|
||||
// unlog from the SSO
|
||||
window.location.href = url;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user