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( isBrowser ? (localStorage.getItem(TOKEN_KEY) ?? undefined) : undefined ); // Token that is ready to use const [tokenStr, setTokenStr] = useState(''); const [errorSession, setErrorSession] = useState< SessionErrorType | undefined >(undefined); const [token, setToken] = useState(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 }; };