diff --git a/src/components/auth/sign-in/sign-in.tsx b/src/components/auth/sign-in/sign-in.tsx index cc12a37..c625170 100644 --- a/src/components/auth/sign-in/sign-in.tsx +++ b/src/components/auth/sign-in/sign-in.tsx @@ -24,8 +24,8 @@ type Props = { export const SignIn = (props: Props) => { const { control, handleSubmit } = useForm({ defaultValues: { - email: '', - password: '', + email: 'test@test.com', + password: 'test', rememberMe: false, }, mode: 'onSubmit', diff --git a/src/router.tsx b/src/router.tsx index 18b6627..d8f292a 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -34,7 +34,7 @@ const privateRoutes: RouteObject[] = [ }, ] -const router = createBrowserRouter([ +export const router = createBrowserRouter([ { children: [ { diff --git a/src/services/auth/auth.service.ts b/src/services/auth/auth.service.ts index f0226e5..9fff9bd 100644 --- a/src/services/auth/auth.service.ts +++ b/src/services/auth/auth.service.ts @@ -1,15 +1,33 @@ import { flashcardsApi } from '..' -import { LoginArgs, User } from './auth.types' +import { LoginArgs, LoginResponse, User } from './auth.types' export const authService = flashcardsApi.injectEndpoints({ endpoints: builder => ({ - login: builder.mutation({ - invalidatesTags: ['Me'], - query: body => ({ - body, - method: 'POST', - url: '/v1/auth/login', - }), + login: builder.mutation({ + async onQueryStarted( + // 1 параметр: QueryArg - аргументы, которые приходят в query + _, + // 2 параметр: MutationLifecycleApi - dispatch, queryFulfilled, getState и пр. + // queryFulfilled - это промис, возвращаемый RTK Query, который разрешается, + // когда запрос успешно завершен + { queryFulfilled } + ) { + const { data } = await queryFulfilled + + if (!data) { + return + } + + localStorage.setItem('accessToken', data.accessToken) + localStorage.setItem('refreshToken', data.refreshToken) + }, + query: body => { + return { + body, + method: 'POST', + url: '/v1/auth/login', + } + }, }), me: builder.query({ providesTags: ['Me'], diff --git a/src/services/auth/auth.types.ts b/src/services/auth/auth.types.ts index fa5be7f..74687cf 100644 --- a/src/services/auth/auth.types.ts +++ b/src/services/auth/auth.types.ts @@ -3,6 +3,12 @@ export type LoginArgs = { password: string rememberMe?: boolean } + +export type LoginResponse = { + accessToken: string + refreshToken: string +} + export type User = { avatar: null | string created: string diff --git a/src/services/flashCardsBaseQuery.ts b/src/services/flashCardsBaseQuery.ts new file mode 100644 index 0000000..c47c5be --- /dev/null +++ b/src/services/flashCardsBaseQuery.ts @@ -0,0 +1,81 @@ +import { router } from '@/router' +import { + BaseQueryFn, + FetchArgs, + FetchBaseQueryError, + fetchBaseQuery, +} from '@reduxjs/toolkit/query/react' +import { Mutex } from 'async-mutex' + +// create a new mutex +const mutex = new Mutex() + +const baseQuery = fetchBaseQuery({ + baseUrl: 'https://api.flashcards.andrii.es', + prepareHeaders: headers => { + const token = localStorage.getItem('accessToken') + + if (headers.get('Authorization')) { + return headers + } + + if (token) { + headers.set('Authorization', `Bearer ${token}`) + } + + return headers + }, +}) + +export const baseQueryWithReauth: BaseQueryFn< + FetchArgs | string, + unknown, + FetchBaseQueryError +> = async (args, api, extraOptions) => { + // wait until the mutex is available without locking it + await mutex.waitForUnlock() + let result = await baseQuery(args, api, extraOptions) + + if (result.error && result.error.status === 401) { + // checking whether the mutex is locked + if (!mutex.isLocked()) { + const release = await mutex.acquire() + + try { + const refreshToken = localStorage.getItem('refreshToken') + const refreshResult = (await baseQuery( + { + headers: { + Authorization: `Bearer ${refreshToken}`, + }, + method: 'POST', + url: '/v2/auth/refresh-token', + }, + api, + extraOptions + )) as any + + console.log('refreshResult', refreshResult) + + if (refreshResult.data) { + localStorage.setItem('accessToken', refreshResult.data.accessToken) + localStorage.setItem('refreshToken', refreshResult.data.refreshToken) + + // retry the initial query + result = await baseQuery(args, api, extraOptions) + } else { + router.navigate('/login') + } + } finally { + // release must be called once the mutex should be released again. + release() + } + } else { + // wait until the mutex is available without locking it + await mutex.waitForUnlock() + result = await baseQuery(args, api, extraOptions) + } + } + + return result +} diff --git a/src/services/flashcardsApi.ts b/src/services/flashcardsApi.ts index f6dc786..4be2f25 100644 --- a/src/services/flashcardsApi.ts +++ b/src/services/flashcardsApi.ts @@ -1,13 +1,8 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' +import { baseQueryWithReauth } from '@/services/flashCardsBaseQuery' +import { createApi } from '@reduxjs/toolkit/query/react' export const flashcardsApi = createApi({ - baseQuery: fetchBaseQuery({ - baseUrl: 'https://api.flashcards.andrii.es', - credentials: 'include', - prepareHeaders: headers => { - headers.append('x-auth-skip', 'true') - }, - }), + baseQuery: baseQueryWithReauth, endpoints: () => ({}), reducerPath: 'flashcardsApi', tagTypes: ['Decks', 'Me'],