karusic/front/src/service/session.ts

262 lines
7.9 KiB
TypeScript

import { useCallback, useEffect, useMemo, useState } from 'react';
import { createListCollection } from '@chakra-ui/react';
import {
JwtToken,
PartRight,
RESTConfig,
RestErrorResponse,
UserResource,
setErrorApiGlobalCallback,
} from '@/back-api';
import { toaster } from '@/components/ui/toaster';
import { environment, getApiUrl } from '@/environment';
import { useServiceContext } from '@/service/ServiceContext';
import { isBrowser } from '@/utils/layout';
import { parseToken } from '@/utils/sso';
const TOKEN_KEY = 'karusic-token-key-storage';
export const USERS_COLLECTION = createListCollection({
items: [
{
label: 'admin',
value:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
},
{ label: 'NO_USER', value: 'svelte' },
],
});
export const USERS = {
admin:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxIiwiYXBwbGljYXRpb24iOiJrYXJ1c2ljIiwiaXNzIjoiS2FyQXV0aCIsInJpZ2h0Ijp7ImthcnVzaWMiOnsiQURNSU4iOnRydWUsIlVTRVIiOnRydWV9fSwibG9naW4iOiJIZWVyb1l1aSIsImV4cCI6MTcyNDIwNjc5NCwiaWF0IjoxNzI0MTY2ODM0fQ.TEST_SIGNATURE_FOR_LOCAL_TEST_AND_TEST_E2E',
NO_USER: '',
} as const;
export const getUserToken = () => {
return localStorage.getItem(TOKEN_KEY);
};
export type RightGroup = 'ADMIN' | 'USER';
export function getRestConfig(): RESTConfig {
return {
server: getApiUrl(),
token: getUserToken() ?? '',
};
}
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: RightGroup) => boolean;
hasWriteRight: (part: RightGroup) => boolean;
getRestConfig: () => RESTConfig;
};
export const useSessionService = (): SessionServiceProps => {
const { session } = useServiceContext();
return session;
};
export const useSessionServiceWrapped = (): SessionServiceProps => {
// Load the token from the system (init)
const [tokenStorage, setTokenStorage] = useState<string | undefined>(
isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? 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);
const getRestConfigLocal = useCallback((): RESTConfig => {
return {
server: getApiUrl(),
token: tokenStr,
};
}, [tokenStr]);
const updateRight = useEffect(() => {
//console.log('Detect a new token...');
if (isBrowser) {
//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)');
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: {
server: getApiUrl(),
token: tokenStorage,
},
})
.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: RestErrorResponse) => {
setErrorSession({
message: 'The server reject the token.',
description: `name: ${error.name}\nmessage: "${error.message}"`,
});
setTokenStr('');
setToken(undefined);
localStorage.removeItem(TOKEN_KEY);
});
}
}
}, [
localStorage,
parseToken,
setErrorSession,
setToken,
setTokenStr,
tokenStorage,
]);
const setTokenLocal = useCallback(
(token?: string) => {
if (token ? token.startsWith('__') : false) {
token = undefined;
}
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, 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: RightGroup) => {
//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]);
},
[token]
);
const hasWriteRight = useCallback(
(part: RightGroup) => {
const right = token?.payload?.right?.[environment.applName];
if (right === undefined) {
return false;
}
return [PartRight.READ, PartRight.READ_WRITE].includes(right[part]);
},
[token]
);
return {
token: tokenStr,
isConnected: token !== undefined,
errorSession,
setToken: setTokenLocal,
clearToken,
login: token?.payload?.login,
hasReadRight,
hasWriteRight,
getRestConfig: getRestConfigLocal,
};
};
export const useHasRight = (part: RightGroup) => {
const { token, hasReadRight, hasWriteRight } = useSessionService();
const isReadable = useMemo(() => {
//console.log(`get is read for: ${part} ==> ${hasReadRight(part)}`);
return hasReadRight(part);
}, [token, hasReadRight, part]);
const isWritable = useMemo(() => {
return hasWriteRight(part);
}, [token, hasWriteRight, part]);
return { isReadable, isWritable };
};