diff --git a/README.md b/README.md index a1edfb19..fec625bb 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,4 @@ _Link to the backend repo:_ https://github.com/chertik77/TaskPro-backend ## Languages and Tools -![Languages and Tools](https://skills.syvixor.com/api/icons?i=ts,react,dndkit,radixui,tanstack,stanjs,axios,datefns,reactdatepicker,reacthookform,valibot,tailwind,tailwindmerge,commitlint,eslint,prettier,githubactions,fsd,yarn,vercel,vite,vscode,figma&perline=8) +![Languages and Tools](https://skills.syvixor.com/api/icons?i=ts,react,dndkit,radixui,tanstack,axios,datefns,reactdatepicker,reacthookform,valibot,tailwind,tailwindmerge,commitlint,eslint,prettier,githubactions,fsd,yarn,vercel,vite,vscode,figma&perline=8) diff --git a/package.json b/package.json index 43459a21..4859c615 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@radix-ui/react-select": "^2.2.5", "@radix-ui/react-slot": "^1.2.3", "@tanstack/react-query": "^5.85.0", + "@tanstack/react-query-devtools": "^5.85.3", "@tanstack/react-router": "1.131.8", "@vercel/analytics": "^1.5.0", "@vercel/speed-insights": "^1.2.0", diff --git a/src/app/providers/RouteProvider.tsx b/src/app/providers/RouteProvider.tsx index a0e6e691..f12a65cd 100644 --- a/src/app/providers/RouteProvider.tsx +++ b/src/app/providers/RouteProvider.tsx @@ -1,16 +1,19 @@ -import { RouterProvider as TanStackRouterProvider } from '@tanstack/react-router' +import { useQueryClient } from '@tanstack/react-query' +import { RouterProvider as TanstackRouterProvider } from '@tanstack/react-router' -import { useSessionStore } from '@/entities/session' +import { sessionService } from '@/entities/session' +import { userQueries } from '@/entities/user' +import { attachInternalApiMemoryStorage } from '@/shared/api' import { router } from '@/shared/lib' export const RouterProvider = () => { - const session = useSessionStore() + const queryClient = useQueryClient() - return ( - - ) + attachInternalApiMemoryStorage({ + refreshTokens: sessionService.refreshTokens, + logout: () => queryClient.resetQueries({ queryKey: userQueries.current() }) + }) + + return } diff --git a/src/app/providers/ToastProvider.tsx b/src/app/providers/ToastProvider.tsx index 4e2b29bf..8a2844c6 100644 --- a/src/app/providers/ToastProvider.tsx +++ b/src/app/providers/ToastProvider.tsx @@ -1,23 +1,19 @@ import { Toaster } from 'sonner' -import { useSessionStore } from '@/entities/session' - import { useTabletAndBelowMediaQuery } from '@/shared/lib' export const ToastProvider = () => { - const { - user: { theme } - } = useSessionStore() - const isTabletAndBelow = useTabletAndBelowMediaQuery() + // const { data: user } = useQuery(userQueries.me()) + return ( ) diff --git a/src/app/routes/__root.tsx b/src/app/routes/__root.tsx index a2c73acf..4fa5982b 100644 --- a/src/app/routes/__root.tsx +++ b/src/app/routes/__root.tsx @@ -1,35 +1,38 @@ -import { createRootRouteWithContext, Outlet } from '@tanstack/react-router' +import type { QueryClient } from '@tanstack/react-query' + +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' +import { + createRootRouteWithContext, + Outlet, + redirect +} from '@tanstack/react-router' import { Analytics } from '@vercel/analytics/react' import { SpeedInsights } from '@vercel/speed-insights/react' -import { sessionService, useSessionStore } from '@/entities/session' - -import { attachInternalApiMemoryStorage } from '@/shared/api' +import { userQueries } from '@/entities/user' type RouterContext = { - session: ReturnType + queryClient: QueryClient } -const RootRoute = () => { - const { tokens, setTokens, logout } = useSessionStore() - - attachInternalApiMemoryStorage({ - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - refreshTokens: sessionService.refreshTokens, - setTokens, - logout - }) - - return ( - <> - - - - - ) -} +const RootRoute = () => ( + <> + + + + + +) export const Route = createRootRouteWithContext()({ + beforeLoad: async ({ context: { queryClient } }) => { + try { + const user = await queryClient.fetchQuery(userQueries.me()) + + if (user) throw redirect({ to: '/dashboard' }) + } catch { + return + } + }, component: RootRoute }) diff --git a/src/app/routes/auth/_auth-layout.tsx b/src/app/routes/auth/_auth-layout.tsx index a82c477b..84a962ef 100644 --- a/src/app/routes/auth/_auth-layout.tsx +++ b/src/app/routes/auth/_auth-layout.tsx @@ -1,10 +1,14 @@ import { createFileRoute, Outlet, redirect } from '@tanstack/react-router' +import { userQueries } from '@/entities/user' + import { AuthNavigation } from '@/widgets/auth-navigation' export const Route = createFileRoute('/auth/_auth-layout')({ - beforeLoad: ({ context: { session } }) => { - if (session.isAuthenticated) throw redirect({ to: '/dashboard' }) + beforeLoad: ({ context: { queryClient } }) => { + const isAuthenticated = queryClient.getQueryData(userQueries.current()) + + if (isAuthenticated) throw redirect({ to: '/dashboard' }) }, component: () => (
diff --git a/src/app/routes/auth/google.callback.tsx b/src/app/routes/auth/google.callback.tsx deleted file mode 100644 index e2e6148c..00000000 --- a/src/app/routes/auth/google.callback.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { GoogleCallbackPage } from '@/pages/google-callback' -import { createFileRoute, redirect } from '@tanstack/react-router' - -import { SessionContracts } from '@/entities/session' - -export const Route = createFileRoute('/auth/google/callback')({ - component: GoogleCallbackPage, - validateSearch: SessionContracts.GoogleCallbackSearchSchema, - beforeLoad({ search: { code, state } }) { - if (!code || !state) throw redirect({ to: '/' }) - } -}) diff --git a/src/app/routes/dashboard/layout.tsx b/src/app/routes/dashboard/layout.tsx index 598e888e..fdecdd4f 100644 --- a/src/app/routes/dashboard/layout.tsx +++ b/src/app/routes/dashboard/layout.tsx @@ -1,9 +1,13 @@ import { DashboardPage } from '@/pages/dashboard' import { createFileRoute, redirect } from '@tanstack/react-router' +import { userQueries } from '@/entities/user' + export const Route = createFileRoute('/dashboard')({ - beforeLoad: ({ context: { session } }) => { - if (!session.isAuthenticated) throw redirect({ to: '/' }) + beforeLoad: ({ context: { queryClient } }) => { + const isAuthenticated = queryClient.getQueryData(userQueries.current()) + + if (!isAuthenticated) throw redirect({ to: '/' }) }, component: DashboardPage }) diff --git a/src/app/routes/index.tsx b/src/app/routes/index.tsx index 1ec616f6..07967599 100644 --- a/src/app/routes/index.tsx +++ b/src/app/routes/index.tsx @@ -1,9 +1,13 @@ import { WelcomePage } from '@/pages/welcome' import { createFileRoute, redirect } from '@tanstack/react-router' +import { userQueries } from '@/entities/user' + export const Route = createFileRoute('/')({ - beforeLoad: ({ context: { session } }) => { - if (session.isAuthenticated) throw redirect({ to: '/dashboard' }) + beforeLoad: ({ context: { queryClient } }) => { + const isAuthenticated = queryClient.getQueryData(userQueries.current()) + + if (isAuthenticated) throw redirect({ to: '/dashboard' }) }, component: WelcomePage }) diff --git a/src/entities/board/ui/FormBgImageSelector.tsx b/src/entities/board/ui/FormBgImageSelector.tsx index 40b26baa..05664ed7 100644 --- a/src/entities/board/ui/FormBgImageSelector.tsx +++ b/src/entities/board/ui/FormBgImageSelector.tsx @@ -2,7 +2,7 @@ import type { ControllerRenderProps, FieldValues } from 'react-hook-form' import { RadioGroup, RadioGroupItem } from '@radix-ui/react-radio-group' -import { useSessionStore } from '@/entities/session/@x/board' +import { useMe } from '@/entities/user/@x/board' import { FormControl, FormItem } from '@/shared/ui' @@ -12,9 +12,7 @@ export const FormBgImageSelector = ({ value, onChange }: ControllerRenderProps) => { - const { - user: { theme } - } = useSessionStore() + const { theme } = useMe() return ( diff --git a/src/entities/session/@x/board.ts b/src/entities/session/@x/board.ts deleted file mode 100644 index d4c537f2..00000000 --- a/src/entities/session/@x/board.ts +++ /dev/null @@ -1 +0,0 @@ -export { useSessionStore } from '../model/store' diff --git a/src/entities/session/api/contracts.ts b/src/entities/session/api/contracts.ts index 00920c20..3a90fcf4 100644 --- a/src/entities/session/api/contracts.ts +++ b/src/entities/session/api/contracts.ts @@ -16,19 +16,6 @@ export const InitiateGoogleResponseDtoSchema = v.object({ redirectUrl: v.pipe(v.string(), v.url()) }) -export const GoogleSigninDtoSchema = v.object({ - code: v.pipe(v.string(), v.minLength(1)), - state: v.pipe(v.string(), v.minLength(1)) -}) - -export const RefreshTokenDtoSchema = v.object({ - refreshToken: v.pipe(v.string(), v.minLength(1)) -}) - export const SessionResponseDtoSchema = v.object({ - accessToken: v.string(), - refreshToken: v.string(), user: v.lazy(() => UserDtoSchema) }) - -export const TokensDtoSchema = v.omit(SessionResponseDtoSchema, ['user']) diff --git a/src/entities/session/api/endpoints.ts b/src/entities/session/api/endpoints.ts index cc5a5d87..ca72bc27 100644 --- a/src/entities/session/api/endpoints.ts +++ b/src/entities/session/api/endpoints.ts @@ -6,7 +6,6 @@ class SessionApiEndpoints { refresh = `${this.baseUrl}/refresh` logout = `${this.baseUrl}/logout` googleInitiate = `${this.baseUrl}/google/initiate` - googleCallback = `${this.baseUrl}/google/callback` } export const sessionApiEndpoints = new SessionApiEndpoints() diff --git a/src/entities/session/api/service.ts b/src/entities/session/api/service.ts index 77e673ac..0d27d413 100644 --- a/src/entities/session/api/service.ts +++ b/src/entities/session/api/service.ts @@ -1,22 +1,14 @@ -import type { - GoogleSigninDto, - RefreshTokenDto, - SigninDto, - SignupDto -} from './types' +import type { SigninDto, SignupDto } from './types' import { parse } from 'valibot' import { axiosInstance } from '@/shared/api' import { - GoogleSigninDtoSchema, InitiateGoogleResponseDtoSchema, - RefreshTokenDtoSchema, SessionResponseDtoSchema, SigninDtoSchema, - SignupDtoSchema, - TokensDtoSchema + SignupDtoSchema } from './contracts' import { sessionApiEndpoints } from './endpoints' @@ -49,37 +41,17 @@ export const sessionService = { }, async initiateGoogleSignin() { - const response = await axiosInstance.get(sessionApiEndpoints.googleInitiate) - - const parsedData = parse(InitiateGoogleResponseDtoSchema, response.data) - - return parsedData - }, - - async signinWithGoogle(data: GoogleSigninDto) { - const googleSigninDto = parse(GoogleSigninDtoSchema, data) - const response = await axiosInstance.post( - sessionApiEndpoints.googleCallback, - googleSigninDto + sessionApiEndpoints.googleInitiate ) - const parsedData = parse(SessionResponseDtoSchema, response.data) + const parsedData = parse(InitiateGoogleResponseDtoSchema, response.data) return parsedData }, - async refreshTokens(data: RefreshTokenDto) { - const refreshTokenDto = parse(RefreshTokenDtoSchema, data) - - const response = await axiosInstance.post( - sessionApiEndpoints.refresh, - refreshTokenDto - ) - - const parsedData = parse(TokensDtoSchema, response.data) - - return parsedData + async refreshTokens() { + await axiosInstance.post(sessionApiEndpoints.refresh) }, async logout() { diff --git a/src/entities/session/api/types.ts b/src/entities/session/api/types.ts index 57c47532..b1b76b2b 100644 --- a/src/entities/session/api/types.ts +++ b/src/entities/session/api/types.ts @@ -1,20 +1,14 @@ import type { InferOutput } from 'valibot' import type { - GoogleSigninDtoSchema, InitiateGoogleResponseDtoSchema, - RefreshTokenDtoSchema, SessionResponseDtoSchema, SigninDtoSchema, - SignupDtoSchema, - TokensDtoSchema + SignupDtoSchema } from './contracts' export type SessionResponseDto = InferOutput -export type TokensDto = InferOutput export type SigninDto = InferOutput export type SignupDto = InferOutput -export type GoogleSigninDto = InferOutput -export type RefreshTokenDto = InferOutput export type InitiateGoogleResponseDto = InferOutput< typeof InitiateGoogleResponseDtoSchema > diff --git a/src/entities/session/index.ts b/src/entities/session/index.ts index 074641a5..562db740 100644 --- a/src/entities/session/index.ts +++ b/src/entities/session/index.ts @@ -1,6 +1,3 @@ -export * as SessionTypes from './model/types' export * as SessionDtoTypes from './api/types' -export * as SessionContracts from './model/contracts' export * as SessionDtoContracts from './api/contracts' export { sessionService } from './api/service' -export { useSessionStore, getSessionStore, sessionActions } from './model/store' diff --git a/src/entities/session/model/contracts.ts b/src/entities/session/model/contracts.ts deleted file mode 100644 index 79826a0f..00000000 --- a/src/entities/session/model/contracts.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as v from 'valibot' - -export const GoogleCallbackSearchSchema = v.object({ - code: v.pipe(v.string(), v.minLength(1)), - state: v.pipe(v.string(), v.minLength(1)) -}) diff --git a/src/entities/session/model/store.ts b/src/entities/session/model/store.ts deleted file mode 100644 index a51a8026..00000000 --- a/src/entities/session/model/store.ts +++ /dev/null @@ -1,41 +0,0 @@ -import type { Theme } from '@/shared/config' -import type { SessionResponseDto } from '../api/types' - -import { createStore } from 'stan-js' -import { storage } from 'stan-js/storage' - -import { userService } from '@/entities/user/@x/session' - -import { DEFAULT_THEME } from '@/shared/config' - -export const { - useStore: useSessionStore, - getState: getSessionStore, - actions: sessionActions -} = createStore( - { - user: storage({ - name: '', - email: '', - avatar: '', - theme: DEFAULT_THEME as Theme - }), - tokens: storage({ accessToken: '', refreshToken: '' }), - get isAuthenticated() { - return [this.tokens.accessToken, this.tokens.refreshToken].every(Boolean) - } - }, - ({ actions, reset }) => ({ - authenticate: ({ user, ...tokens }: SessionResponseDto) => { - actions.setUser(user) - actions.setTokens(tokens) - }, - getCurrentUser: async () => { - actions.setUser(await userService.getCurrentUser()) - }, - logout: () => { - reset() - localStorage.clear() - } - }) -) diff --git a/src/entities/session/model/types.ts b/src/entities/session/model/types.ts deleted file mode 100644 index f3a46fdb..00000000 --- a/src/entities/session/model/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { InferOutput } from 'valibot' -import type { GoogleCallbackSearchSchema } from './contracts' - -export type GoogleCallbackSearchSchema = InferOutput< - typeof GoogleCallbackSearchSchema -> diff --git a/src/entities/user/@x/board.ts b/src/entities/user/@x/board.ts new file mode 100644 index 00000000..6ffc88e9 --- /dev/null +++ b/src/entities/user/@x/board.ts @@ -0,0 +1 @@ +export { useMe } from '../model/useMe' diff --git a/src/entities/user/api/queries.ts b/src/entities/user/api/queries.ts new file mode 100644 index 00000000..6ecbbdfe --- /dev/null +++ b/src/entities/user/api/queries.ts @@ -0,0 +1,13 @@ +import { queryOptions } from '@tanstack/react-query' + +import { userService } from './service' + +export const userQueries = { + current: () => ['user'], + me: () => + queryOptions({ + queryKey: userQueries.current(), + queryFn: userService.getMe, + staleTime: Infinity + }) +} diff --git a/src/entities/user/api/service.ts b/src/entities/user/api/service.ts index 3a7e14cf..5f8fa9ff 100644 --- a/src/entities/user/api/service.ts +++ b/src/entities/user/api/service.ts @@ -28,8 +28,10 @@ export const userService = { return parsedData }, - async getCurrentUser() { - const response = await axiosInstance.get(userApiEndpoints.me) + async getMe() { + const response = await axiosInstance.get(userApiEndpoints.me, { + skipAuthRefresh: true + }) const parsedData = parse(UserDtoSchema, response.data) diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts index fe07c4c8..7629721c 100644 --- a/src/entities/user/index.ts +++ b/src/entities/user/index.ts @@ -3,3 +3,5 @@ export * as UserTypes from './model/types' export * as UserDtoContracts from './api/contracts' export * as UserContracts from './model/contracts' export { userService } from './api/service' +export { userQueries } from './api/queries' +export { useMe } from './model/useMe' diff --git a/src/entities/user/model/useMe.ts b/src/entities/user/model/useMe.ts new file mode 100644 index 00000000..13ba3d4e --- /dev/null +++ b/src/entities/user/model/useMe.ts @@ -0,0 +1,9 @@ +import { useSuspenseQuery } from '@tanstack/react-query' + +import { userQueries } from '../api/queries' + +export const useMe = () => { + const { data: user } = useSuspenseQuery(userQueries.me()) + + return user +} diff --git a/src/features/user/change-theme/api/useChangeTheme.ts b/src/features/user/change-theme/api/useChangeTheme.ts index b0d36be2..aa4b6bf3 100644 --- a/src/features/user/change-theme/api/useChangeTheme.ts +++ b/src/features/user/change-theme/api/useChangeTheme.ts @@ -1,31 +1,36 @@ -import type { Theme } from '@/shared/config' - -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { parse } from 'valibot' -import { useSessionStore } from '@/entities/session' -import { UserContracts, userService } from '@/entities/user' +import { UserContracts, userQueries, userService } from '@/entities/user' export const useChangeTheme = () => { - const { user: previousUser, setUser } = useSessionStore() + const queryClient = useQueryClient() return useMutation({ mutationFn: userService.editUser, meta: { errorMessage: 'We couldn’t update your theme. Please try again' }, onMutate: async ({ theme }) => { - const previousTheme = previousUser.theme + await queryClient.cancelQueries({ queryKey: userQueries.current() }) + + const previousUser = queryClient.getQueryData(userQueries.current()) + + const parsedPreviousUser = parse(UserContracts.UserSchema, previousUser) - setUser(prev => ({ ...prev, theme: theme! })) + queryClient.setQueryData(userQueries.current(), oldUser => { + if (!oldUser) return oldUser - return { previousTheme } + const parsedOldUser = parse(UserContracts.UserSchema, oldUser) + + return { ...parsedOldUser, theme } + }) + + return { previousUser: parsedPreviousUser } }, - onError: (_, _variables, context) => { - setUser({ ...previousUser, theme: context?.previousTheme as Theme }) + onError(_, __, context) { + queryClient.setQueryData(userQueries.current(), context?.previousUser) }, - onSettled: data => { - const parsedUser = parse(UserContracts.UserSchema, data) - - setUser(parsedUser) + onSettled() { + queryClient.invalidateQueries({ queryKey: userQueries.current() }) } }) } diff --git a/src/features/user/change-theme/lib/useMetaThemeColor.ts b/src/features/user/change-theme/lib/useMetaThemeColor.ts index ef831a0e..45ff0760 100644 --- a/src/features/user/change-theme/lib/useMetaThemeColor.ts +++ b/src/features/user/change-theme/lib/useMetaThemeColor.ts @@ -1,11 +1,9 @@ import { useEffect } from 'react' -import { useSessionStore } from '@/entities/session' +import { useMe } from '@/entities/user' export const useMetaThemeColor = () => { - const { - user: { theme } - } = useSessionStore() + const { theme } = useMe() useEffect(() => { let themeColorMeta = document.querySelector( diff --git a/src/features/user/change-theme/lib/useThemeWithRootSync.ts b/src/features/user/change-theme/lib/useThemeWithRootSync.ts index 4777d90f..114ae822 100644 --- a/src/features/user/change-theme/lib/useThemeWithRootSync.ts +++ b/src/features/user/change-theme/lib/useThemeWithRootSync.ts @@ -1,13 +1,11 @@ import { useEffect } from 'react' -import { useSessionStore } from '@/entities/session' +import { useMe } from '@/entities/user' import { DEFAULT_THEME } from '@/shared/config' export const useThemeWithRootSync = () => { - const { - user: { theme } - } = useSessionStore() + const { theme } = useMe() useEffect(() => { const root = window.document.documentElement diff --git a/src/features/user/edit-profile/api/useEditProfile.ts b/src/features/user/edit-profile/api/useEditProfile.ts index d488428c..e1bd8dbe 100644 --- a/src/features/user/edit-profile/api/useEditProfile.ts +++ b/src/features/user/edit-profile/api/useEditProfile.ts @@ -2,26 +2,22 @@ import type { Dispatch, SetStateAction } from 'react' import { useMutation } from '@tanstack/react-query' -import { useSessionStore } from '@/entities/session' -import { userService } from '@/entities/user' +import { userQueries, userService } from '@/entities/user' export const useEditProfile = ( setIsDialogOpen?: Dispatch> -) => { - const { setUser } = useSessionStore() - - return useMutation({ +) => + useMutation({ mutationFn: userService.editUser, meta: { + invalidates: [userQueries.current()], successMessage: 'Your profile has been successfully updated.', errorMessage: e => e?.response?.status === 409 ? 'An account with this email address already exists. Please use a different email.' : 'Failed to update profile. Please try again. If the problem persists, contact support.' }, - onSuccess(data) { + onSuccess() { setIsDialogOpen?.(false) - setUser(data) } }) -} diff --git a/src/features/user/edit-profile/lib/useEditProfileForm.ts b/src/features/user/edit-profile/lib/useEditProfileForm.ts index 5ec181ab..53eae244 100644 --- a/src/features/user/edit-profile/lib/useEditProfileForm.ts +++ b/src/features/user/edit-profile/lib/useEditProfileForm.ts @@ -1,13 +1,11 @@ -import { useSessionStore } from '@/entities/session' +import { useMe } from '@/entities/user' import { useAppForm, useIsFormReadyForSubmit } from '@/shared/lib' import { EditUserSchema } from '../model/contract' export const useEditProfileForm = () => { - const { - user: { name, email } - } = useSessionStore() + const { name, email } = useMe() const form = useAppForm(EditUserSchema, { defaultValues: { name, email, password: '' } diff --git a/src/features/user/edit-profile/ui/EditAvatar.tsx b/src/features/user/edit-profile/ui/EditAvatar.tsx index 78e307c1..baac6947 100644 --- a/src/features/user/edit-profile/ui/EditAvatar.tsx +++ b/src/features/user/edit-profile/ui/EditAvatar.tsx @@ -1,15 +1,13 @@ import { useRef } from 'react' -import { useSessionStore } from '@/entities/session' +import { useMe } from '@/entities/user' import { Icon, Loader } from '@/shared/ui' import { useEditProfile } from '../api/useEditProfile' export const EditAvatar = () => { - const { - user: { avatar } - } = useSessionStore() + const { avatar } = useMe() const ref = useRef(null) diff --git a/src/features/user/edit-profile/ui/EditProfileDialogTrigger.tsx b/src/features/user/edit-profile/ui/EditProfileDialogTrigger.tsx index 7af68293..bcf0ee8e 100644 --- a/src/features/user/edit-profile/ui/EditProfileDialogTrigger.tsx +++ b/src/features/user/edit-profile/ui/EditProfileDialogTrigger.tsx @@ -1,11 +1,9 @@ -import { useSessionStore } from '@/entities/session' +import { useMe } from '@/entities/user' import { Avatar, AvatarFallback, AvatarImage, DialogTrigger } from '@/shared/ui' export const EditProfileDialogTrigger = () => { - const { - user: { name, avatar } - } = useSessionStore() + const { name, avatar } = useMe() return ( { useMetaThemeColor() - useEffect(() => { - sessionActions.getCurrentUser() - }, []) - return (
{ - const { code, state } = useSearch({ from: '/auth/google/callback' }) - - const { authenticate } = useSessionStore() - - const navigate = useNavigate() - - return useMutation({ - mutationFn: () => sessionService.signinWithGoogle({ code, state }), - meta: { - errorMessage: - 'An error occurred during sign-in with Google. Please try again shortly.' - }, - onSuccess(data) { - navigate({ to: '/dashboard' }) - authenticate(data) - }, - onError() { - navigate({ to: '/' }) - } - }) -} diff --git a/src/pages/google-callback/index.ts b/src/pages/google-callback/index.ts deleted file mode 100644 index a4f4104d..00000000 --- a/src/pages/google-callback/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { GoogleCallbackPage } from './ui/GoogleCallbackPage' diff --git a/src/pages/google-callback/ui/GoogleCallbackPage.tsx b/src/pages/google-callback/ui/GoogleCallbackPage.tsx deleted file mode 100644 index 1d73eb10..00000000 --- a/src/pages/google-callback/ui/GoogleCallbackPage.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { useEffect } from 'react' - -import { Loader } from '@/shared/ui' - -import { useGoogleSignin } from '../api/useGoogleSignin' - -export const GoogleCallbackPage = () => { - const { mutate, isPending } = useGoogleSignin() - - useEffect(() => { - mutate() - }, [mutate]) - - return ( -
- {isPending && } -
- ) -} diff --git a/src/pages/signin/api/useSigninUser.ts b/src/pages/signin/api/useSigninUser.ts index 4a21326e..f5921835 100644 --- a/src/pages/signin/api/useSigninUser.ts +++ b/src/pages/signin/api/useSigninUser.ts @@ -1,13 +1,14 @@ import type { UseFormReset } from 'react-hook-form' import type { SigninSchema } from '../model/contract' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' -import { sessionService, useSessionStore } from '@/entities/session' +import { sessionService } from '@/entities/session' +import { userQueries } from '@/entities/user' export const useSigninUser = (reset: UseFormReset) => { - const { authenticate } = useSessionStore() + const queryClient = useQueryClient() const navigate = useNavigate() @@ -19,10 +20,10 @@ export const useSigninUser = (reset: UseFormReset) => { ? 'The email or password you entered is incorrect. Please try again.' : 'An error occurred during sign-in. Our technical team has been notified. Please try again shortly.' }, - onSuccess(data) { + onSuccess({ user }) { reset() + queryClient.setQueryData(userQueries.current(), user) navigate({ to: '/dashboard' }) - authenticate(data) } }) } diff --git a/src/pages/signup/api/useSignupUser.ts b/src/pages/signup/api/useSignupUser.ts index 3e9b9c94..d985d80d 100644 --- a/src/pages/signup/api/useSignupUser.ts +++ b/src/pages/signup/api/useSignupUser.ts @@ -1,13 +1,14 @@ import type { UseFormReset } from 'react-hook-form' import type { SignupSchema } from '../model/contract' -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' -import { sessionService, useSessionStore } from '@/entities/session' +import { sessionService } from '@/entities/session' +import { userQueries } from '@/entities/user' export const useSignupUser = (reset: UseFormReset) => { - const { authenticate } = useSessionStore() + const queryClient = useQueryClient() const navigate = useNavigate() @@ -19,10 +20,10 @@ export const useSignupUser = (reset: UseFormReset) => { ? 'An account with this email address already exists. Please sign in or use a different email.' : 'An error occurred during sign-up. Our technical team has been notified. Please try again shortly.' }, - onSuccess(data) { + onSuccess({ user }) { reset() + queryClient.setQueryData(userQueries.current(), user) navigate({ to: '/dashboard' }) - authenticate(data) } }) } diff --git a/src/shared/api/apiMemoryStorage.ts b/src/shared/api/apiMemoryStorage.ts index 58bbe38a..4a28b8c8 100644 --- a/src/shared/api/apiMemoryStorage.ts +++ b/src/shared/api/apiMemoryStorage.ts @@ -1,11 +1,5 @@ -type Tokens = { - accessToken: string - refreshToken: string -} - -type ApiMemoryStorage = Tokens & { - refreshTokens: (data: Pick) => Promise - setTokens: (data: Tokens) => void +type ApiMemoryStorage = { + refreshTokens: () => void logout: () => void } @@ -15,22 +9,10 @@ export const attachInternalApiMemoryStorage = (data: ApiMemoryStorage) => { __internalMemoryStorage = () => data } -export const getApiAccessToken = () => { - const { accessToken } = __internalMemoryStorage() - - return accessToken -} - -export const getRefreshedTokens = () => { - const { refreshTokens, refreshToken } = __internalMemoryStorage() - - return refreshTokens({ refreshToken }) -} - -export const setTokens = (data: Tokens) => { - const { setTokens } = __internalMemoryStorage() +export const refreshTokens = () => { + const { refreshTokens } = __internalMemoryStorage() - return setTokens(data) + return refreshTokens() } export const logUserOut = () => { diff --git a/src/shared/api/instance.ts b/src/shared/api/instance.ts index 5e80b549..8715ac41 100644 --- a/src/shared/api/instance.ts +++ b/src/shared/api/instance.ts @@ -2,25 +2,11 @@ import axios, { AxiosError } from 'axios' import { env } from '../config' import { router } from '../lib' -import { - getApiAccessToken, - getRefreshedTokens, - logUserOut, - setTokens -} from './apiMemoryStorage' +import { logUserOut, refreshTokens } from './apiMemoryStorage' export const axiosInstance = axios.create({ - baseURL: env.VITE_API_BASE_URL -}) - -axiosInstance.interceptors.request.use(config => { - const accessToken = getApiAccessToken() - - if (config?.headers && accessToken) { - config.headers.Authorization = `Bearer ${accessToken}` - } - - return config + baseURL: env.VITE_API_BASE_URL, + withCredentials: true }) axiosInstance.interceptors.response.use( @@ -36,9 +22,7 @@ axiosInstance.interceptors.response.use( originalRequest._retry = true try { - const tokens = await getRefreshedTokens() - - setTokens(tokens) + refreshTokens() return axiosInstance(originalRequest) } catch (e) { diff --git a/src/shared/lib/router/routeTree.gen.ts b/src/shared/lib/router/routeTree.gen.ts index 01ff31b8..d03f7aa2 100644 --- a/src/shared/lib/router/routeTree.gen.ts +++ b/src/shared/lib/router/routeTree.gen.ts @@ -16,7 +16,6 @@ import { Route as IndexRouteImport } from './../../../app/routes/index' import { Route as DashboardIndexRouteImport } from './../../../app/routes/dashboard/index' import { Route as DashboardBoardIdRouteImport } from './../../../app/routes/dashboard/$boardId' import { Route as AuthAuthLayoutRouteImport } from './../../../app/routes/auth/_auth-layout' -import { Route as AuthGoogleCallbackRouteImport } from './../../../app/routes/auth/google.callback' import { Route as AuthAuthLayoutSignupRouteImport } from './../../../app/routes/auth/_auth-layout.signup' import { Route as AuthAuthLayoutSigninRouteImport } from './../../../app/routes/auth/_auth-layout.signin' @@ -51,11 +50,6 @@ const AuthAuthLayoutRoute = AuthAuthLayoutRouteImport.update({ id: '/_auth-layout', getParentRoute: () => AuthRoute, } as any) -const AuthGoogleCallbackRoute = AuthGoogleCallbackRouteImport.update({ - id: '/google/callback', - path: '/google/callback', - getParentRoute: () => AuthRoute, -} as any) const AuthAuthLayoutSignupRoute = AuthAuthLayoutSignupRouteImport.update({ id: '/signup', path: '/signup', @@ -75,7 +69,6 @@ export interface FileRoutesByFullPath { '/dashboard/': typeof DashboardIndexRoute '/auth/signin': typeof AuthAuthLayoutSigninRoute '/auth/signup': typeof AuthAuthLayoutSignupRoute - '/auth/google/callback': typeof AuthGoogleCallbackRoute } export interface FileRoutesByTo { '/': typeof IndexRoute @@ -84,7 +77,6 @@ export interface FileRoutesByTo { '/dashboard': typeof DashboardIndexRoute '/auth/signin': typeof AuthAuthLayoutSigninRoute '/auth/signup': typeof AuthAuthLayoutSignupRoute - '/auth/google/callback': typeof AuthGoogleCallbackRoute } export interface FileRoutesById { __root__: typeof rootRouteImport @@ -96,7 +88,6 @@ export interface FileRoutesById { '/dashboard/': typeof DashboardIndexRoute '/auth/_auth-layout/signin': typeof AuthAuthLayoutSigninRoute '/auth/_auth-layout/signup': typeof AuthAuthLayoutSignupRoute - '/auth/google/callback': typeof AuthGoogleCallbackRoute } export interface FileRouteTypes { fileRoutesByFullPath: FileRoutesByFullPath @@ -108,7 +99,6 @@ export interface FileRouteTypes { | '/dashboard/' | '/auth/signin' | '/auth/signup' - | '/auth/google/callback' fileRoutesByTo: FileRoutesByTo to: | '/' @@ -117,7 +107,6 @@ export interface FileRouteTypes { | '/dashboard' | '/auth/signin' | '/auth/signup' - | '/auth/google/callback' id: | '__root__' | '/' @@ -128,7 +117,6 @@ export interface FileRouteTypes { | '/dashboard/' | '/auth/_auth-layout/signin' | '/auth/_auth-layout/signup' - | '/auth/google/callback' fileRoutesById: FileRoutesById } export interface RootRouteChildren { @@ -181,13 +169,6 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthAuthLayoutRouteImport parentRoute: typeof AuthRoute } - '/auth/google/callback': { - id: '/auth/google/callback' - path: '/google/callback' - fullPath: '/auth/google/callback' - preLoaderRoute: typeof AuthGoogleCallbackRouteImport - parentRoute: typeof AuthRoute - } '/auth/_auth-layout/signup': { id: '/auth/_auth-layout/signup' path: '/signup' @@ -235,12 +216,10 @@ const AuthAuthLayoutRouteWithChildren = AuthAuthLayoutRoute._addFileChildren( interface AuthRouteChildren { AuthAuthLayoutRoute: typeof AuthAuthLayoutRouteWithChildren - AuthGoogleCallbackRoute: typeof AuthGoogleCallbackRoute } const AuthRouteChildren: AuthRouteChildren = { AuthAuthLayoutRoute: AuthAuthLayoutRouteWithChildren, - AuthGoogleCallbackRoute: AuthGoogleCallbackRoute, } const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren) diff --git a/src/shared/lib/router/router.ts b/src/shared/lib/router/router.ts index 22faa1e0..ba4ccf9a 100644 --- a/src/shared/lib/router/router.ts +++ b/src/shared/lib/router/router.ts @@ -1,10 +1,11 @@ import { createRouter } from '@tanstack/react-router' +import { queryClient } from '../query/query-client' import { routeTree } from './routeTree.gen' export const router = createRouter({ routeTree, defaultPreload: 'intent', defaultPendingMinMs: 0, - context: { session: undefined! } + context: { queryClient } }) diff --git a/src/widgets/sidebar/api/useLogoutUser.ts b/src/widgets/sidebar/api/useLogoutUser.ts index 0f37da05..37fd04bb 100644 --- a/src/widgets/sidebar/api/useLogoutUser.ts +++ b/src/widgets/sidebar/api/useLogoutUser.ts @@ -1,10 +1,11 @@ -import { useMutation } from '@tanstack/react-query' +import { useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from '@tanstack/react-router' -import { sessionService, useSessionStore } from '@/entities/session' +import { sessionService } from '@/entities/session' +import { userQueries } from '@/entities/user' export const useLogoutUser = () => { - const { logout } = useSessionStore() + const queryClient = useQueryClient() const navigate = useNavigate() @@ -16,7 +17,7 @@ export const useLogoutUser = () => { }, onSuccess() { navigate({ to: '/' }) - logout() + queryClient.resetQueries({ queryKey: userQueries.current() }) } }) } diff --git a/yarn.lock b/yarn.lock index 96558e7c..552f9b7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1814,6 +1814,18 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.83.1.tgz#eed82970b30cb24536f561613b5630e03d349628" integrity sha512-OG69LQgT7jSp+5pPuCfzltq/+7l2xoweggjme9vlbCPa/d7D7zaqv5vN/S82SzSYZ4EDLTxNO1PWrv49RAS64Q== +"@tanstack/query-devtools@5.84.0": + version "5.84.0" + resolved "https://registry.yarnpkg.com/@tanstack/query-devtools/-/query-devtools-5.84.0.tgz#dabed4f4abf405f0e320758ed7bc895a7687d9bb" + integrity sha512-fbF3n+z1rqhvd9EoGp5knHkv3p5B2Zml1yNRjh7sNXklngYI5RVIWUrUjZ1RIcEoscarUb0+bOvIs5x9dwzOXQ== + +"@tanstack/react-query-devtools@^5.85.3": + version "5.85.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query-devtools/-/react-query-devtools-5.85.3.tgz#9ea854c744e96667917fcdc13d9d87f029bb4ab2" + integrity sha512-WSVweCE1Kh1BVvPDHAmLgGT+GGTJQ9+a7bVqzD+zUiUTht+salJjYm5nikpMNaHFPJV102TCYdvgHgBXtURRNg== + dependencies: + "@tanstack/query-devtools" "5.84.0" + "@tanstack/react-query@^5.85.0": version "5.85.0" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.85.0.tgz#4f60315b3809228c0396b67c9cd3651896172542"