diff --git a/backend/src/main.ts b/backend/src/main.ts index 90bd5946..f886cfcc 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -4,7 +4,7 @@ import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger"; import { ValidationPipe } from "@nestjs/common"; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { cors: true }); // Swagger const document = SwaggerModule.createDocument( diff --git a/backend/src/users/types/user-domain.type.ts b/backend/src/users/types/user-domain.type.ts index f2b281c1..31d3e3eb 100644 --- a/backend/src/users/types/user-domain.type.ts +++ b/backend/src/users/types/user-domain.type.ts @@ -5,6 +5,8 @@ export class UserDomain { id: string; @ApiProperty({ type: String, description: "Nickname of user" }) nickname: string; + @ApiProperty({ type: String, description: "Last worksace ID of user" }) + lastWorkspaceId: string; @ApiProperty({ type: Date, description: "Created date of user" }) createdAt: Date; @ApiProperty({ type: Date, description: "Updated date of user" }) diff --git a/backend/src/users/users.service.ts b/backend/src/users/users.service.ts index b0b158c7..70964b34 100644 --- a/backend/src/users/users.service.ts +++ b/backend/src/users/users.service.ts @@ -2,13 +2,26 @@ import { Injectable } from "@nestjs/common"; import { User } from "@prisma/client"; import { PrismaService } from "src/db/prisma.service"; import { FindUserResponse } from "./types/find-user-response.type"; +import { WorkspaceRoleConstants } from "src/utils/constants/auth-role"; @Injectable() export class UsersService { constructor(private prismaService: PrismaService) {} async findOne(userId: string): Promise { - return await this.prismaService.user.findUnique({ + const foundUserWorkspace = await this.prismaService.userWorkspace.findFirst({ + select: { + workspaceId: true, + }, + where: { + userId, + }, + orderBy: { + id: "desc", + }, + }); + + const foundUser = await this.prismaService.user.findUnique({ select: { id: true, nickname: true, @@ -19,6 +32,11 @@ export class UsersService { id: userId, }, }); + + return { + ...foundUser, + lastWorkspaceId: foundUserWorkspace.workspaceId, + }; } async findOrCreate( @@ -26,17 +44,39 @@ export class UsersService { socialUid: string, nickname: string ): Promise { - return this.prismaService.user.upsert({ + const foundUser = await this.prismaService.user.findFirst({ where: { socialProvider, socialUid, }, - update: {}, - create: { + }); + + if (foundUser) { + return foundUser; + } + + const user = await this.prismaService.user.create({ + data: { socialProvider, socialUid, nickname, }, }); + + const workspace = await this.prismaService.workspace.create({ + data: { + title: `${user.nickname}'s Workspace`, + }, + }); + + await this.prismaService.userWorkspace.create({ + data: { + userId: user.id, + workspaceId: workspace.id, + role: WorkspaceRoleConstants.OWNER, + }, + }); + + return user; } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 41934754..77b9d425 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,9 +19,11 @@ "@react-hook/window-size": "^3.1.1", "@reduxjs/toolkit": "^2.0.1", "@swc/helpers": "^0.5.3", + "@tanstack/react-query": "^5.17.15", "@uiw/codemirror-theme-xcode": "^4.21.21", "@uiw/codemirror-themes": "^4.21.21", "@uiw/react-markdown-preview": "^5.0.7", + "axios": "^1.6.5", "codemirror": "^6.0.1", "codemirror-markdown-commands": "^0.0.3", "codemirror-markdown-slug": "^0.0.3", @@ -1912,6 +1914,30 @@ "tslib": "^2.4.0" } }, + "node_modules/@tanstack/query-core": { + "version": "5.17.15", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.15.tgz", + "integrity": "sha512-QURxpu77/ICA4d61aPvV7EcJ2MwmksxUejKBaq/xLcO2TUJAlXf4PFKHC/WxnVFI/7F1jeLx85AO3Vpk0+uBXw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.17.15", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.17.15.tgz", + "integrity": "sha512-9qur91mOihaUN7pXm6ioDtS+4qgkBcCiIaZyvi3lZNcQZsrMGCYZ+eP3hiFrV4khoJyJrFUX1W0NcCVlgwNZxQ==", + "dependencies": { + "@tanstack/query-core": "5.17.15" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18.0.0" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -2450,6 +2476,21 @@ "node": ">=8" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.5.tgz", + "integrity": "sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg==", + "dependencies": { + "follow-redirects": "^1.15.4", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2740,6 +2781,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -2848,6 +2900,14 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3412,6 +3472,38 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/follow-redirects": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", + "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -5057,6 +5149,25 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", @@ -5389,6 +5500,11 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 41376f9d..020c2925 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,9 +23,11 @@ "@react-hook/window-size": "^3.1.1", "@reduxjs/toolkit": "^2.0.1", "@swc/helpers": "^0.5.3", + "@tanstack/react-query": "^5.17.15", "@uiw/codemirror-theme-xcode": "^4.21.21", "@uiw/codemirror-themes": "^4.21.21", "@uiw/react-markdown-preview": "^5.0.7", + "axios": "^1.6.5", "codemirror": "^6.0.1", "codemirror-markdown-commands": "^0.0.3", "codemirror-markdown-slug": "^0.0.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a1bf6210..1ea930ba 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,40 +6,14 @@ import "./App.css"; import { Box, CssBaseline, ThemeProvider, createTheme, useMediaQuery } from "@mui/material"; import { useSelector } from "react-redux"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; -import EditorLayout from "./components/layouts/EditorLayout"; -import EditorIndex from "./pages/editor/Index"; import { useMemo } from "react"; import { selectConfig } from "./store/configSlice"; -import MainLayout from "./components/layouts/MainLayout"; -import Index from "./pages/Index"; -import CallbackIndex from "./pages/auth/callback/Index"; +import axios from "axios"; +import { routes } from "./routes"; -const router = createBrowserRouter([ - { - path: "", - element: , - children: [ - { - path: "", - element: , - }, - ], - }, - { - path: ":documentId", - element: , - children: [ - { - path: "", - element: , - }, - ], - }, - { - path: "auth/callback", - element: , - }, -]); +const router = createBrowserRouter(routes); + +axios.defaults.baseURL = import.meta.env.VITE_API_ADDR; function App() { const config = useSelector(selectConfig); diff --git a/frontend/src/components/common/GuestRoute.tsx b/frontend/src/components/common/GuestRoute.tsx new file mode 100644 index 00000000..43a2645f --- /dev/null +++ b/frontend/src/components/common/GuestRoute.tsx @@ -0,0 +1,30 @@ +import { ReactNode, useContext } from "react"; +import { useLocation, Navigate } from "react-router-dom"; +import { AuthContext } from "../../contexts/AuthContext"; +import { useSelector } from "react-redux"; +import { selectUser } from "../../store/userSlice"; + +interface RejectLoggedInRouteProps { + children?: ReactNode; +} + +const GuestRoute = (props: RejectLoggedInRouteProps) => { + const { children } = props; + const { isLoggedIn } = useContext(AuthContext); + const location = useLocation(); + const userStore = useSelector(selectUser); + + if (isLoggedIn) { + return ( + + ); + } + + return children; +}; + +export default GuestRoute; diff --git a/frontend/src/components/common/PrivateRoute.tsx b/frontend/src/components/common/PrivateRoute.tsx new file mode 100644 index 00000000..ba56620d --- /dev/null +++ b/frontend/src/components/common/PrivateRoute.tsx @@ -0,0 +1,30 @@ +import { ReactNode, useContext } from "react"; +import { useLocation, Navigate } from "react-router-dom"; +import { AuthContext } from "../../contexts/AuthContext"; +import { Backdrop, CircularProgress } from "@mui/material"; + +interface PrivateRouteProps { + children?: ReactNode; +} + +const PrivateRoute = (props: PrivateRouteProps) => { + const { children } = props; + const { isLoggedIn, isLoading } = useContext(AuthContext); + const location = useLocation(); + + if (isLoading) { + return ( + + + + ); + } + + if (!isLoggedIn) { + return ; + } + + return children; +}; + +export default PrivateRoute; diff --git a/frontend/src/components/drawers/WorkspaceDrawer.tsx b/frontend/src/components/drawers/WorkspaceDrawer.tsx new file mode 100644 index 00000000..f4a0d589 --- /dev/null +++ b/frontend/src/components/drawers/WorkspaceDrawer.tsx @@ -0,0 +1,48 @@ +import { + Avatar, + Box, + Divider, + Drawer, + ListItem, + ListItemAvatar, + ListItemButton, + ListItemText, +} from "@mui/material"; +import { useSelector } from "react-redux"; +import { selectUser } from "../../store/userSlice"; + +const DRAWER_WIDTH = 240; + +function WorkspaceDrawer() { + const userStore = useSelector(selectUser); + + return ( + + + + + + + {userStore.data?.nickname.charAt(0)} + + + + + + + ); +} + +export default WorkspaceDrawer; diff --git a/frontend/src/components/layouts/WorkspaceLayout.tsx b/frontend/src/components/layouts/WorkspaceLayout.tsx new file mode 100644 index 00000000..c77152a5 --- /dev/null +++ b/frontend/src/components/layouts/WorkspaceLayout.tsx @@ -0,0 +1,7 @@ +import { Outlet } from "react-router-dom"; + +function WorkspaceLayout() { + return ; +} + +export default WorkspaceLayout; diff --git a/frontend/src/contexts/AuthContext.ts b/frontend/src/contexts/AuthContext.ts new file mode 100644 index 00000000..015d5209 --- /dev/null +++ b/frontend/src/contexts/AuthContext.ts @@ -0,0 +1,11 @@ +import React from "react"; + +export interface AuthContextValue { + isLoading: boolean; + isLoggedIn: boolean; +} + +export const AuthContext = React.createContext({ + isLoading: true, + isLoggedIn: false, +}); diff --git a/frontend/src/hooks/api/types/user.d.ts b/frontend/src/hooks/api/types/user.d.ts new file mode 100644 index 00000000..bf4223c2 --- /dev/null +++ b/frontend/src/hooks/api/types/user.d.ts @@ -0,0 +1,8 @@ +export interface User { + id: string; + nickname: string; + createdAt: Date; + updatedAt: Date; +} + +export class GetUserResponse extends User {} diff --git a/frontend/src/hooks/api/user.ts b/frontend/src/hooks/api/user.ts new file mode 100644 index 00000000..902f0f5f --- /dev/null +++ b/frontend/src/hooks/api/user.ts @@ -0,0 +1,41 @@ +import { useDispatch, useSelector } from "react-redux"; +import { selectAuth, setAccessToken } from "../../store/authSlice"; +import { useQuery } from "@tanstack/react-query"; +import axios from "axios"; +import { GetUserResponse } from "./types/user"; +import { useEffect } from "react"; +import { User, setUserData } from "../../store/userSlice"; + +export const generateGetUserQueryKey = (accessToken: string) => { + return ["users", accessToken]; +}; + +export const useGetUserQuery = () => { + const dispatch = useDispatch(); + const authStore = useSelector(selectAuth); + + if (authStore.accessToken) { + axios.defaults.headers.common["Authorization"] = `Bearer ${authStore.accessToken}`; + } + + const query = useQuery({ + queryKey: generateGetUserQueryKey(authStore.accessToken || ""), + enabled: Boolean(authStore.accessToken), + queryFn: async () => { + const res = await axios.get("/users"); + return res.data; + }, + }); + + useEffect(() => { + if (query.isSuccess) { + dispatch(setUserData(query.data as User)); + } else if (query.isError) { + dispatch(setAccessToken(null)); + dispatch(setUserData(null)); + axios.defaults.headers.common["Authorization"] = ""; + } + }, [dispatch, query.data, query.isError, query.isSuccess]); + + return query; +}; diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f59de3d2..a793cbfb 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,14 +6,21 @@ import { store } from "./store/store"; import { Provider } from "react-redux"; import { PersistGate } from "redux-persist/integration/react"; import { persistStore } from "redux-persist"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import AuthProvider from "./providers/AuthProvider"; const persistor = persistStore(store); +const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")!).render( - + + + + + diff --git a/frontend/src/pages/workspace/Index.tsx b/frontend/src/pages/workspace/Index.tsx new file mode 100644 index 00000000..302ccd00 --- /dev/null +++ b/frontend/src/pages/workspace/Index.tsx @@ -0,0 +1,7 @@ +import WorkspaceDrawer from "../../components/drawers/WorkspaceDrawer"; + +function WorkspaceIndex() { + return ; +} + +export default WorkspaceIndex; diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx new file mode 100644 index 00000000..ef2fd8ab --- /dev/null +++ b/frontend/src/providers/AuthProvider.tsx @@ -0,0 +1,20 @@ +import { ReactNode } from "react"; +import { AuthContext } from "../contexts/AuthContext"; +import { useGetUserQuery } from "../hooks/api/user"; + +interface AuthProviderProps { + children?: ReactNode; +} + +function AuthProvider(props: AuthProviderProps) { + const { children } = props; + const { isSuccess, isLoading } = useGetUserQuery(); + + return ( + + {children} + + ); +} + +export default AuthProvider; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx new file mode 100644 index 00000000..ff06a4bb --- /dev/null +++ b/frontend/src/routes.tsx @@ -0,0 +1,70 @@ +import EditorLayout from "./components/layouts/EditorLayout"; +import EditorIndex from "./pages/editor/Index"; +import MainLayout from "./components/layouts/MainLayout"; +import Index from "./pages/Index"; +import CallbackIndex from "./pages/auth/callback/Index"; +import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; +import GuestRoute from "./components/common/GuestRoute"; +import PrivateRoute from "./components/common/PrivateRoute"; +import WorkspaceIndex from "./pages/workspace/Index"; + +const enum AccessType { + PRIVATE, // Authroized user can access only + PUBLIC, // Everyone can access + GUEST, // Not authorized user can access only +} + +const codePairRoutes = [ + { + path: "", + accessType: AccessType.GUEST, + element: , + children: [ + { + path: "", + element: , + }, + ], + }, + { + path: "workspace", + accessType: AccessType.PRIVATE, + element: , + children: [ + { + path: ":workspaceId", + element: , + }, + ], + }, + { + path: ":documentId", + accessType: AccessType.PUBLIC, + element: , + children: [ + { + path: "", + element: , + }, + ], + }, + { + path: "auth/callback", + accessType: AccessType.GUEST, + element: , + }, +]; + +const injectProtectedRoute = (routes: typeof codePairRoutes) => { + return routes.map((route) => { + if (route.accessType === AccessType.PRIVATE) { + route.element = {route.element}; + } else if (route.accessType === AccessType.GUEST) { + route.element = {route.element}; + } + + return route; + }); +}; + +export const routes = injectProtectedRoute(codePairRoutes); diff --git a/frontend/src/store/authSlice.ts b/frontend/src/store/authSlice.ts index 219e933c..4ecf3371 100644 --- a/frontend/src/store/authSlice.ts +++ b/frontend/src/store/authSlice.ts @@ -2,11 +2,11 @@ import { createSlice } from "@reduxjs/toolkit"; import type { PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "./store"; -export interface ConfigState { +export interface AuthState { accessToken: string | null; } -const initialState: ConfigState = { +const initialState: AuthState = { accessToken: null, }; @@ -22,6 +22,6 @@ export const authSlice = createSlice({ export const { setAccessToken } = authSlice.actions; -export const selectConfig = (state: RootState) => state.config; +export const selectAuth = (state: RootState) => state.auth; export default authSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index a8703fd5..72519d1a 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -4,12 +4,14 @@ import configSlice from "./configSlice"; import storage from "redux-persist/lib/storage"; import { persistReducer } from "redux-persist"; import authSlice from "./authSlice"; +import userSlice from "./userSlice"; const reducers = combineReducers({ // Persistence auth: authSlice, config: configSlice, // Volatile + user: userSlice, editor: editorSlice, }); diff --git a/frontend/src/store/userSlice.ts b/frontend/src/store/userSlice.ts new file mode 100644 index 00000000..0734fb31 --- /dev/null +++ b/frontend/src/store/userSlice.ts @@ -0,0 +1,35 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "./store"; + +export interface User { + id: string; + nickname: string; + lastWorkspaceId: string; + updatedAt: Date; + createdAt: Date; +} + +export interface UserState { + data: User | null; +} + +const initialState: UserState = { + data: null, +}; + +export const userSlice = createSlice({ + name: "user", + initialState, + reducers: { + setUserData: (state, action: PayloadAction) => { + state.data = action.payload; + }, + }, +}); + +export const { setUserData } = userSlice.actions; + +export const selectUser = (state: RootState) => state.user; + +export default userSlice.reducer;