diff --git a/hooks/useAuthentication.ts b/hooks/useAuthentication.ts new file mode 100644 index 0000000..18f743a --- /dev/null +++ b/hooks/useAuthentication.ts @@ -0,0 +1,222 @@ +'use client'; +import {Dispatch, SetStateAction, useContext, useEffect, useState} from 'react'; +import {AppRouterInstance, useRouter} from '@/lib/navigation'; +import {useTranslations} from '@/lib/i18n'; +import {AlertContext, AlertContextProps} from '@/context/AlertContext'; +import {LangContext, LangContextProps} from '@/context/LangContext'; +import OfflineContext, {OfflineMode} from '@/context/OfflineContext'; +import {SessionProps} from '@/lib/types/session'; +import {UserProps} from '@/lib/types/user'; +import {apiGet} from '@/lib/api/client'; +import {isDesktop} from '@/lib/configs'; +import * as tauri from '@/lib/tauri'; +import {getCookie} from '@/lib/utils/cookies'; + +interface UseAuthenticationReturn { + session: SessionProps; + setSession: Dispatch>; + isLoading: boolean; + currentCredits: number; + setCurrentCredits: Dispatch>; + amountSpent: number; + setAmountSpent: Dispatch>; + showPinSetup: boolean; + setShowPinSetup: Dispatch>; + showPinVerify: boolean; + setShowPinVerify: Dispatch>; + handlePinVerifySuccess: (userId: string) => Promise; +} + +export default function useAuthentication(): UseAuthenticationReturn { + const t = useTranslations(); + const {lang: locale}: LangContextProps = useContext(LangContext); + const {errorMessage}: AlertContextProps = useContext(AlertContext); + const {initializeDatabase, setOfflineMode} = useContext(OfflineContext); + const router: AppRouterInstance = useRouter(); + + const [session, setSession] = useState({user: null, accessToken: '', isConnected: false}); + const [currentCredits, setCurrentCredits] = useState(0); + const [amountSpent, setAmountSpent] = useState(session.user?.aiUsage || 0); + const [isLoading, setIsLoading] = useState(true); + const [sessionAttempts, setSessionAttempts] = useState(0); + const [showPinSetup, setShowPinSetup] = useState(false); + const [showPinVerify, setShowPinVerify] = useState(false); + + useEffect((): void => { + checkAuthentification().then(); + }, []); + + useEffect((): void => { + if (session.isConnected) { + setIsLoading(false); + } else { + if (sessionAttempts > 2) { + router.push('/'); + } + } + setSessionAttempts(sessionAttempts + 1); + }, [session]); + + async function checkAuthentification(): Promise { + const token: string | null = isDesktop ? await tauri.getToken() : getCookie('token'); + + if (token) { + try { + const user: UserProps = await apiGet('user/infos', token, locale); + if (!user) { + errorMessage(t('homePage.errors.userNotFound')); + if (isDesktop) { + await tauri.removeToken(); + await tauri.logout(); + } + return; + } + + if (isDesktop && user.id) { + try { + const initResult = await tauri.initUser(user.id); + if (!initResult.success) { + errorMessage(initResult.error || t('homePage.errors.offlineInitError')); + return; + } + try { + const offlineStatus = await tauri.offlineModeGet(); + if (!offlineStatus.hasPin && user.termsAccepted) { + setTimeout((): void => { + setShowPinSetup(true); + }, 2000); + } + } catch (error) { + errorMessage(t('homePage.errors.offlineModeError')); + } + } catch (error) { + errorMessage(t('homePage.errors.offlineInitError')); + } + + try { + const dbInitialized: boolean = await initializeDatabase(user.id); + if (dbInitialized) { + try { + await tauri.syncUser({ + userId: user.id, + username: user.username || '', + email: user.email || '' + }); + } catch (syncError) { + errorMessage(t('homePage.errors.syncError')); + } + } + } catch (error) { + errorMessage(t('homePage.errors.syncError')); + } + } + + setSession({ + isConnected: true, + user: user, + accessToken: token, + }); + setCurrentCredits(user.creditsBalance); + setAmountSpent(user.aiUsage); + } catch (e: unknown) { + if (isDesktop) { + try { + const offlineStatus = await tauri.offlineModeGet(); + if (offlineStatus.hasPin && offlineStatus.lastUserId) { + setOfflineMode((prev: OfflineMode): OfflineMode => ({ + ...prev, + isOffline: true, + isNetworkOnline: false + })); + setShowPinVerify(true); + setIsLoading(false); + return; + } else { + await tauri.removeToken(); + await tauri.logout(); + } + } catch (offlineError) { + errorMessage(t('homePage.errors.offlineError')); + } + } else { + window.location.href = 'https://eritors.com/login'; + } + + if (e instanceof Error) { + errorMessage(e.message); + } else { + errorMessage(t('homePage.errors.authenticationError')); + } + } + } else { + if (isDesktop) { + try { + const offlineStatus = await tauri.offlineModeGet(); + if (offlineStatus.hasPin && offlineStatus.lastUserId) { + setOfflineMode((prev: OfflineMode): OfflineMode => ({ + ...prev, + isOffline: true, + isNetworkOnline: false + })); + setShowPinVerify(true); + setIsLoading(false); + return; + } + } catch (error) { + errorMessage(t('homePage.errors.authenticationError')); + } + await tauri.openLoginWindow(); + } else { + window.location.href = 'https://eritors.com/login'; + } + } + } + + async function handlePinVerifySuccess(userId: string): Promise { + try { + const storedToken: string | null = await tauri.getToken(); + const encryptionKey: string | null = await tauri.getUserEncryptionKey(userId); + + if (encryptionKey) { + await tauri.dbInitialize(userId, encryptionKey); + setOfflineMode((prev: OfflineMode): OfflineMode => ({ + ...prev, + isDatabaseInitialized: true + })); + + const localUser: UserProps = await tauri.getUserInfo(); + if (localUser && localUser.id) { + setSession({ + isConnected: true, + user: localUser, + accessToken: storedToken || '', + }); + setShowPinVerify(false); + setCurrentCredits(localUser.creditsBalance || 0); + setAmountSpent(localUser.aiUsage || 0); + } else { + errorMessage(t('homePage.errors.localDataError')); + } + } else { + errorMessage(t('homePage.errors.encryptionKeyError')); + } + } catch (error) { + errorMessage(t('homePage.errors.offlineModeError')); + } + } + + return { + session, + setSession, + isLoading, + currentCredits, + setCurrentCredits, + amountSpent, + setAmountSpent, + showPinSetup, + setShowPinSetup, + showPinVerify, + setShowPinVerify, + handlePinVerifySuccess, + }; +} diff --git a/lib/navigation.ts b/lib/navigation.ts index b23fe62..7916006 100644 --- a/lib/navigation.ts +++ b/lib/navigation.ts @@ -1,6 +1,12 @@ import {useNavigate, useParams as useRouterParams, Link, useLocation} from 'react-router-dom'; -function useRouter() { +export type AppRouterInstance = { + push: (path: string) => void; + replace: (path: string) => void; + back: () => void; +}; + +function useRouter(): AppRouterInstance { const navigate = useNavigate(); return { push: (path: string) => navigate(path),