diff --git a/src/services/api/services/auth.ts b/src/services/api/services/auth.ts index 4ffd2b7d..4732b265 100644 --- a/src/services/api/services/auth.ts +++ b/src/services/api/services/auth.ts @@ -1,5 +1,4 @@ import { useCallback } from "react"; -import useFetchBase from "../use-fetch-base"; import useFetch from "../use-fetch"; import { API_URL } from "../config"; import { User } from "../types/user"; @@ -17,7 +16,7 @@ export type AuthLoginResponse = Tokens & { }; export function useAuthLoginService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthLoginRequest) => { @@ -39,7 +38,7 @@ export type AuthGoogleLoginResponse = Tokens & { }; export function useAuthGoogleLoginService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthGoogleLoginRequest) => { @@ -61,7 +60,7 @@ export type AuthFacebookLoginResponse = Tokens & { }; export function useAuthFacebookLoginService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthFacebookLoginRequest, requestConfig?: RequestConfigType) => { @@ -83,7 +82,7 @@ export type AuthSignUpRequest = { export type AuthSignUpResponse = void; export function useAuthSignUpService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthSignUpRequest, requestConfig?: RequestConfigType) => { @@ -104,7 +103,7 @@ export type AuthConfirmEmailRequest = { export type AuthConfirmEmailResponse = void; export function useAuthConfirmEmailService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthConfirmEmailRequest, requestConfig?: RequestConfigType) => { @@ -125,7 +124,7 @@ export type AuthConfirmNewEmailRequest = { export type AuthConfirmNewEmailResponse = void; export function useAuthConfirmNewEmailService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthConfirmNewEmailRequest, requestConfig?: RequestConfigType) => { @@ -146,7 +145,7 @@ export type AuthForgotPasswordRequest = { export type AuthForgotPasswordResponse = void; export function useAuthForgotPasswordService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthForgotPasswordRequest, requestConfig?: RequestConfigType) => { @@ -168,7 +167,7 @@ export type AuthResetPasswordRequest = { export type AuthResetPasswordResponse = void; export function useAuthResetPasswordService() { - const fetchBase = useFetchBase(); + const fetchBase = useFetch(); return useCallback( (data: AuthResetPasswordRequest, requestConfig?: RequestConfigType) => { diff --git a/src/services/api/use-fetch-base.ts b/src/services/api/use-fetch-base.ts deleted file mode 100644 index 4e6b6af3..00000000 --- a/src/services/api/use-fetch-base.ts +++ /dev/null @@ -1,78 +0,0 @@ -"use client"; - -import { useCallback } from "react"; -import { Tokens } from "./types/tokens"; -import { TokensInfo } from "../auth/auth-context"; -import { AUTH_REFRESH_URL } from "./config"; -import { FetchInputType, FetchInitType } from "./types/fetch-params"; -import useLanguage from "../i18n/use-language"; - -function useFetchBase() { - const language = useLanguage(); - - return useCallback( - async ( - input: FetchInputType, - init?: FetchInitType, - tokens?: Tokens & { - setTokensInfo?: (tokensInfo: TokensInfo) => void; - } - ) => { - let headers: HeadersInit = { - "x-custom-lang": language, - }; - - if (!(init?.body instanceof FormData)) { - headers = { - ...headers, - "Content-Type": "application/json", - }; - } - - if (tokens?.token) { - headers = { - ...headers, - Authorization: `Bearer ${tokens.token}`, - }; - } - - if (tokens?.tokenExpires && tokens.tokenExpires <= Date.now()) { - const newTokens = await fetch(AUTH_REFRESH_URL, { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${tokens.refreshToken}`, - }, - }).then((res) => res.json()); - - if (newTokens.token) { - tokens?.setTokensInfo?.({ - token: newTokens.token, - refreshToken: newTokens.refreshToken, - tokenExpires: newTokens.tokenExpires, - }); - - headers = { - ...headers, - Authorization: `Bearer ${newTokens.token}`, - }; - } else { - tokens?.setTokensInfo?.(null); - - throw new Error("Refresh token expired"); - } - } - - return fetch(input, { - ...init, - headers: { - ...headers, - ...init?.headers, - }, - }); - }, - [language] - ); -} - -export default useFetchBase; diff --git a/src/services/api/use-fetch.ts b/src/services/api/use-fetch.ts index 75eff210..c5d5395b 100644 --- a/src/services/api/use-fetch.ts +++ b/src/services/api/use-fetch.ts @@ -1,27 +1,69 @@ "use client"; import { useCallback } from "react"; -import useFetchBase from "./use-fetch-base"; -import useAuthTokens from "../auth/use-auth-tokens"; -import { FetchInitType, FetchInputType } from "./types/fetch-params"; +import { AUTH_REFRESH_URL } from "./config"; +import { FetchInputType, FetchInitType } from "./types/fetch-params"; +import useLanguage from "../i18n/use-language"; +import { getTokensInfo, setTokensInfo } from "../auth/auth-tokens-info"; function useFetch() { - const { tokensInfoRef, setTokensInfo } = useAuthTokens(); - const fetchBase = useFetchBase(); + const language = useLanguage(); - const fetchWrapper = useCallback( + return useCallback( async (input: FetchInputType, init?: FetchInitType) => { - return fetchBase(input, init, { - token: tokensInfoRef.current?.token, - refreshToken: tokensInfoRef.current?.refreshToken, - tokenExpires: tokensInfoRef.current?.tokenExpires, - setTokensInfo, + const tokens = getTokensInfo(); + + let headers: HeadersInit = { + "x-custom-lang": language, + }; + + if (!(init?.body instanceof FormData)) { + headers = { + ...headers, + "Content-Type": "application/json", + }; + } + + if (tokens?.token) { + headers = { + ...headers, + Authorization: `Bearer ${tokens.token}`, + }; + } + + if (tokens?.tokenExpires && tokens.tokenExpires - 60000 <= Date.now()) { + const newTokens = await fetch(AUTH_REFRESH_URL, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${tokens.refreshToken}`, + }, + }).then((res) => res.json()); + + if (newTokens.token) { + setTokensInfo({ + token: newTokens.token, + refreshToken: newTokens.refreshToken, + tokenExpires: newTokens.tokenExpires, + }); + + headers = { + ...headers, + Authorization: `Bearer ${newTokens.token}`, + }; + } + } + + return fetch(input, { + ...init, + headers: { + ...headers, + ...init?.headers, + }, }); }, - [fetchBase, setTokensInfo, tokensInfoRef] + [language] ); - - return fetchWrapper; } export default useFetch; diff --git a/src/services/auth/auth-context.ts b/src/services/auth/auth-context.ts index 72ba2c32..fcbc5e6d 100644 --- a/src/services/auth/auth-context.ts +++ b/src/services/auth/auth-context.ts @@ -23,15 +23,7 @@ export const AuthActionsContext = createContext<{ }); export const AuthTokensContext = createContext<{ - tokensInfoRef: React.MutableRefObject; setTokensInfo: (tokensInfo: TokensInfo) => void; }>({ - tokensInfoRef: { - current: { - token: null, - refreshToken: null, - tokenExpires: null, - }, - }, setTokensInfo: () => {}, }); diff --git a/src/services/auth/auth-provider.tsx b/src/services/auth/auth-provider.tsx index 5d681d41..408bbc7b 100644 --- a/src/services/auth/auth-provider.tsx +++ b/src/services/auth/auth-provider.tsx @@ -1,13 +1,11 @@ "use client"; -import { Tokens } from "@/services/api/types/tokens"; import { User } from "@/services/api/types/user"; import { PropsWithChildren, useCallback, useEffect, useMemo, - useRef, useState, } from "react"; import { @@ -16,90 +14,46 @@ import { AuthTokensContext, TokensInfo, } from "./auth-context"; -import Cookies from "js-cookie"; -import useFetchBase from "@/services/api/use-fetch-base"; +import useFetch from "@/services/api/use-fetch"; import { AUTH_LOGOUT_URL, AUTH_ME_URL } from "@/services/api/config"; import HTTP_CODES_ENUM from "../api/types/http-codes"; +import { + getTokensInfo, + setTokensInfo as setTokensInfoToStorage, +} from "./auth-tokens-info"; function AuthProvider(props: PropsWithChildren<{}>) { - const AUTH_TOKEN_KEY = "auth-token-data"; - const [tabId] = useState(() => Math.random().toString(36).slice(2)); - const [broadcastChannel] = useState( - () => new BroadcastChannel(AUTH_TOKEN_KEY) - ); const [isLoaded, setIsLoaded] = useState(false); const [user, setUser] = useState(null); - const tokensInfoRef = useRef({ - token: null, - refreshToken: null, - tokenExpires: null, - }); - const fetchBase = useFetchBase(); - - const setTokensInfoRef = useCallback((tokens: TokensInfo) => { - tokensInfoRef.current = tokens ?? { - token: null, - refreshToken: null, - tokenExpires: null, - }; - }, []); + const fetchBase = useFetch(); - const setTokensInfo = useCallback( - (tokensInfo: TokensInfo) => { - setTokensInfoRef(tokensInfo); - broadcastChannel.postMessage({ - tabId, - tokens: tokensInfo, - }); + const setTokensInfo = useCallback((tokensInfo: TokensInfo) => { + setTokensInfoToStorage(tokensInfo); - if (tokensInfo) { - Cookies.set(AUTH_TOKEN_KEY, JSON.stringify(tokensInfo)); - } else { - Cookies.remove(AUTH_TOKEN_KEY); - setUser(null); - } - }, - [setTokensInfoRef, broadcastChannel, tabId] - ); + if (!tokensInfo) { + setUser(null); + } + }, []); const logOut = useCallback(async () => { - if (tokensInfoRef.current.token) { - await fetchBase( - AUTH_LOGOUT_URL, - { - method: "POST", - }, - { - token: tokensInfoRef.current.token, - refreshToken: tokensInfoRef.current.refreshToken, - tokenExpires: tokensInfoRef.current.tokenExpires, - } - ); + const tokens = getTokensInfo(); + + if (tokens?.token) { + await fetchBase(AUTH_LOGOUT_URL, { + method: "POST", + }); } setTokensInfo(null); }, [setTokensInfo, fetchBase]); const loadData = useCallback(async () => { - const tokens = JSON.parse( - Cookies.get(AUTH_TOKEN_KEY) ?? "null" - ) as TokensInfo; - - setTokensInfoRef(tokens); + const tokens = getTokensInfo(); try { if (tokens?.token) { - const response = await fetchBase( - AUTH_ME_URL, - { - method: "GET", - }, - { - token: tokens.token, - refreshToken: tokens.refreshToken, - tokenExpires: tokens.tokenExpires, - setTokensInfo, - } - ); + const response = await fetchBase(AUTH_ME_URL, { + method: "GET", + }); if (response.status === HTTP_CODES_ENUM.UNAUTHORIZED) { logOut(); @@ -109,37 +63,15 @@ function AuthProvider(props: PropsWithChildren<{}>) { const data = await response.json(); setUser(data); } - } catch { - logOut(); } finally { setIsLoaded(true); } - }, [fetchBase, logOut, setTokensInfoRef, setTokensInfo]); + }, [fetchBase, logOut]); useEffect(() => { loadData(); }, [loadData]); - useEffect(() => { - const onMessage = ( - event: MessageEvent<{ - tabId: string; - tokens: TokensInfo; - }> - ) => { - if (event.data.tabId === tabId) return; - - if (!event.data.tokens) setUser(null); - setTokensInfoRef(event.data.tokens); - }; - - broadcastChannel.addEventListener("message", onMessage); - - return () => { - broadcastChannel.removeEventListener("message", onMessage); - }; - }, [broadcastChannel, setTokensInfoRef, tabId]); - const contextValue = useMemo( () => ({ isLoaded, @@ -158,7 +90,6 @@ function AuthProvider(props: PropsWithChildren<{}>) { const contextTokensValue = useMemo( () => ({ - tokensInfoRef, setTokensInfo, }), [setTokensInfo] diff --git a/src/services/auth/auth-tokens-info.ts b/src/services/auth/auth-tokens-info.ts new file mode 100644 index 00000000..1345ec68 --- /dev/null +++ b/src/services/auth/auth-tokens-info.ts @@ -0,0 +1,15 @@ +import { TokensInfo } from "./auth-context"; +import Cookies from "js-cookie"; +import { AUTH_TOKEN_KEY } from "./config"; + +export function getTokensInfo() { + return JSON.parse(Cookies.get(AUTH_TOKEN_KEY) ?? "null") as TokensInfo; +} + +export function setTokensInfo(tokens: TokensInfo) { + if (tokens) { + Cookies.set(AUTH_TOKEN_KEY, JSON.stringify(tokens)); + } else { + Cookies.remove(AUTH_TOKEN_KEY); + } +} diff --git a/src/services/auth/config.ts b/src/services/auth/config.ts index c04f0aef..24b97b45 100644 --- a/src/services/auth/config.ts +++ b/src/services/auth/config.ts @@ -1,2 +1,4 @@ export const IS_SIGN_UP_ENABLED = process.env.NEXT_PUBLIC_IS_SIGN_UP_ENABLED === "true"; + +export const AUTH_TOKEN_KEY = "auth-token-data"; diff --git a/src/services/helpers/remove-duplicates-from-array-of-objects.ts b/src/services/helpers/remove-duplicates-from-array-of-objects.ts index ca3bd2c7..0406dfa6 100644 --- a/src/services/helpers/remove-duplicates-from-array-of-objects.ts +++ b/src/services/helpers/remove-duplicates-from-array-of-objects.ts @@ -2,7 +2,7 @@ function removeDuplicatesFromArrayObjects(array: T[], key: keyof T | never) { const lookup = new Set(); return array.filter( - (value) => !lookup.has(value[key]) && lookup.add(value[key]) + (value) => !lookup.has(value?.[key]) && lookup.add(value?.[key]) ); }