From 5696e972548a37c5fb096af9fb1c05f52e8944fa Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 13:34:00 +0900 Subject: [PATCH 1/9] Add react query --- frontend/package-lock.json | 25 +++++++++++++++++++++++++ frontend/package.json | 1 + frontend/src/main.tsx | 6 +++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 41934754..f72a8aee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "@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", @@ -1912,6 +1913,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", diff --git a/frontend/package.json b/frontend/package.json index 41376f9d..adbd6dd5 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -23,6 +23,7 @@ "@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", diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index f59de3d2..51016e3a 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -6,14 +6,18 @@ 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"; const persistor = persistStore(store); +const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")!).render( - + + + From e0ca253272b001ab5bfe69c78480658c77dcf47f Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 13:35:39 +0900 Subject: [PATCH 2/9] Add axios deps --- frontend/package-lock.json | 91 ++++++++++++++++++++++++++++++++++++++ frontend/package.json | 1 + 2 files changed, 92 insertions(+) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f72a8aee..77b9d425 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,6 +23,7 @@ "@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", @@ -2475,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", @@ -2765,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", @@ -2873,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", @@ -3437,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", @@ -5082,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", @@ -5414,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 adbd6dd5..020c2925 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "@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", From 52a094a49888311b97d05caf50a6acd1f4e39b48 Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 14:39:27 +0900 Subject: [PATCH 3/9] Save user data in store --- backend/src/main.ts | 2 +- frontend/src/App.tsx | 3 ++ frontend/src/contexts/AuthContext.ts | 9 ++++++ frontend/src/hooks/api/types/user.d.ts | 8 +++++ frontend/src/hooks/api/user.ts | 41 +++++++++++++++++++++++++ frontend/src/main.tsx | 5 ++- frontend/src/providers/AuthProvider.tsx | 21 +++++++++++++ frontend/src/store/authSlice.ts | 6 ++-- frontend/src/store/store.ts | 2 ++ frontend/src/store/userSlice.ts | 34 ++++++++++++++++++++ 10 files changed, 126 insertions(+), 5 deletions(-) create mode 100644 frontend/src/contexts/AuthContext.ts create mode 100644 frontend/src/hooks/api/types/user.d.ts create mode 100644 frontend/src/hooks/api/user.ts create mode 100644 frontend/src/providers/AuthProvider.tsx create mode 100644 frontend/src/store/userSlice.ts 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/frontend/src/App.tsx b/frontend/src/App.tsx index a1bf6210..42b6f769 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -13,6 +13,7 @@ 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"; const router = createBrowserRouter([ { @@ -41,6 +42,8 @@ const router = createBrowserRouter([ }, ]); +axios.defaults.baseURL = import.meta.env.VITE_API_ADDR; + function App() { const config = useSelector(selectConfig); const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); diff --git a/frontend/src/contexts/AuthContext.ts b/frontend/src/contexts/AuthContext.ts new file mode 100644 index 00000000..ba10d5fa --- /dev/null +++ b/frontend/src/contexts/AuthContext.ts @@ -0,0 +1,9 @@ +import React from "react"; + +export interface AuthContextValue { + isLoggedIn: boolean; +} + +export const AuthContext = React.createContext({ + 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 51016e3a..a793cbfb 100755 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -7,6 +7,7 @@ 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(); @@ -16,7 +17,9 @@ ReactDOM.createRoot(document.getElementById("root")!).render( - + + + diff --git a/frontend/src/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx new file mode 100644 index 00000000..07af63c8 --- /dev/null +++ b/frontend/src/providers/AuthProvider.tsx @@ -0,0 +1,21 @@ +import { ReactNode, useEffect, useState } 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 } = useGetUserQuery(); + const [isLoggedIn, setIsLoggedIn] = useState(false); + + useEffect(() => { + setIsLoggedIn(isSuccess); + }, [isSuccess]); + + return {children}; +} + +export default AuthProvider; 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..4dac1243 --- /dev/null +++ b/frontend/src/store/userSlice.ts @@ -0,0 +1,34 @@ +import { createSlice } from "@reduxjs/toolkit"; +import type { PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "./store"; + +export interface User { + id: string; + nickname: 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; From 89cddbf69222fc0e00dad0203b2dd3e5a8334519 Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 15:03:08 +0900 Subject: [PATCH 4/9] Add protected routes --- frontend/src/App.tsx | 33 +------------ .../src/components/common/ProtectedRoute.tsx | 30 ++++++++++++ frontend/src/contexts/AuthContext.ts | 2 + frontend/src/providers/AuthProvider.tsx | 15 +++--- frontend/src/routes.tsx | 48 +++++++++++++++++++ 5 files changed, 89 insertions(+), 39 deletions(-) create mode 100644 frontend/src/components/common/ProtectedRoute.tsx create mode 100644 frontend/src/routes.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 42b6f769..1ea930ba 100755 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,41 +6,12 @@ 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; diff --git a/frontend/src/components/common/ProtectedRoute.tsx b/frontend/src/components/common/ProtectedRoute.tsx new file mode 100644 index 00000000..b547e0ae --- /dev/null +++ b/frontend/src/components/common/ProtectedRoute.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 RequireAuthProps { + children?: ReactNode; +} + +const ProtectedRoute = (props: RequireAuthProps) => { + const { children } = props; + const { isLoggedIn, isLoading } = useContext(AuthContext); + const location = useLocation(); + + if (isLoading) { + return ( + + + + ); + } + + if (!isLoggedIn) { + return ; + } + + return children; +}; + +export default ProtectedRoute; diff --git a/frontend/src/contexts/AuthContext.ts b/frontend/src/contexts/AuthContext.ts index ba10d5fa..015d5209 100644 --- a/frontend/src/contexts/AuthContext.ts +++ b/frontend/src/contexts/AuthContext.ts @@ -1,9 +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/providers/AuthProvider.tsx b/frontend/src/providers/AuthProvider.tsx index 07af63c8..ef2fd8ab 100644 --- a/frontend/src/providers/AuthProvider.tsx +++ b/frontend/src/providers/AuthProvider.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useState } from "react"; +import { ReactNode } from "react"; import { AuthContext } from "../contexts/AuthContext"; import { useGetUserQuery } from "../hooks/api/user"; @@ -8,14 +8,13 @@ interface AuthProviderProps { function AuthProvider(props: AuthProviderProps) { const { children } = props; - const { isSuccess } = useGetUserQuery(); - const [isLoggedIn, setIsLoggedIn] = useState(false); + const { isSuccess, isLoading } = useGetUserQuery(); - useEffect(() => { - setIsLoggedIn(isSuccess); - }, [isSuccess]); - - return {children}; + return ( + + {children} + + ); } export default AuthProvider; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx new file mode 100644 index 00000000..1a6b3d76 --- /dev/null +++ b/frontend/src/routes.tsx @@ -0,0 +1,48 @@ +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 ProtectedRoute from "./components/common/ProtectedRoute"; + +const codePairRoutes = [ + { + path: "/", + private: false, + element: , + children: [ + { + path: "/", + element: , + }, + ], + }, + { + path: "/:documentId", + private: true, + element: , + children: [ + { + path: "", + element: , + }, + ], + }, + { + path: "auth/callback", + private: false, + element: , + }, +]; + +const injectProtectedRoute = (routes: typeof codePairRoutes) => { + return routes.map((route) => { + if (route.private) { + route.element = {route.element}; + } + + return route; + }); +}; + +export const routes = injectProtectedRoute(codePairRoutes); From e7e70b4892dcb86b069fdab7223a32cef5d8639e Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 15:16:53 +0900 Subject: [PATCH 5/9] Add workspace page --- frontend/src/components/layouts/WorkspaceLayout.tsx | 7 +++++++ frontend/src/pages/workspace/Index.tsx | 7 +++++++ frontend/src/routes.tsx | 12 ++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 frontend/src/components/layouts/WorkspaceLayout.tsx create mode 100644 frontend/src/pages/workspace/Index.tsx 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/pages/workspace/Index.tsx b/frontend/src/pages/workspace/Index.tsx new file mode 100644 index 00000000..29529361 --- /dev/null +++ b/frontend/src/pages/workspace/Index.tsx @@ -0,0 +1,7 @@ +import { Container } from "@mui/material"; + +function WorkspaceIndex() { + return ; +} + +export default WorkspaceIndex; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 1a6b3d76..4d5dc744 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -4,6 +4,7 @@ import MainLayout from "./components/layouts/MainLayout"; import Index from "./pages/Index"; import CallbackIndex from "./pages/auth/callback/Index"; import ProtectedRoute from "./components/common/ProtectedRoute"; +import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; const codePairRoutes = [ { @@ -17,6 +18,17 @@ const codePairRoutes = [ }, ], }, + { + path: "/workspace", + private: true, + element: , + children: [ + { + path: "/", + element: , + }, + ], + }, { path: "/:documentId", private: true, From a396f47f83fa88ad84c5fbbc0acb9b536c4bfabd Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 15:31:16 +0900 Subject: [PATCH 6/9] Add lastWorksapceId to user controller --- backend/src/users/types/user-domain.type.ts | 2 + backend/src/users/users.service.ts | 48 +++++++++++++++++++-- 2 files changed, 46 insertions(+), 4 deletions(-) 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; } } From 255bc3b5adedea2419b1beb5cdd5235125171ca7 Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 15:32:30 +0900 Subject: [PATCH 7/9] Change routes paths --- frontend/src/routes.tsx | 10 +++++----- frontend/src/store/userSlice.ts | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 4d5dc744..dfc738df 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -8,29 +8,29 @@ import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; const codePairRoutes = [ { - path: "/", + path: "", private: false, element: , children: [ { - path: "/", + path: "", element: , }, ], }, { - path: "/workspace", + path: "workspace", private: true, element: , children: [ { - path: "/", + path: "", element: , }, ], }, { - path: "/:documentId", + path: ":documentId", private: true, element: , children: [ diff --git a/frontend/src/store/userSlice.ts b/frontend/src/store/userSlice.ts index 4dac1243..0734fb31 100644 --- a/frontend/src/store/userSlice.ts +++ b/frontend/src/store/userSlice.ts @@ -5,6 +5,7 @@ import { RootState } from "./store"; export interface User { id: string; nickname: string; + lastWorkspaceId: string; updatedAt: Date; createdAt: Date; } From da50f9d5d97b615e73690c241cfb2e4ac8c22547 Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 15:43:29 +0900 Subject: [PATCH 8/9] Add workspace page --- frontend/src/components/common/GuestRoute.tsx | 30 +++++++++++++++++++ .../{ProtectedRoute.tsx => PrivateRoute.tsx} | 6 ++-- frontend/src/routes.tsx | 25 +++++++++++----- 3 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/common/GuestRoute.tsx rename frontend/src/components/common/{ProtectedRoute.tsx => PrivateRoute.tsx} (83%) 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/ProtectedRoute.tsx b/frontend/src/components/common/PrivateRoute.tsx similarity index 83% rename from frontend/src/components/common/ProtectedRoute.tsx rename to frontend/src/components/common/PrivateRoute.tsx index b547e0ae..ba56620d 100644 --- a/frontend/src/components/common/ProtectedRoute.tsx +++ b/frontend/src/components/common/PrivateRoute.tsx @@ -3,11 +3,11 @@ import { useLocation, Navigate } from "react-router-dom"; import { AuthContext } from "../../contexts/AuthContext"; import { Backdrop, CircularProgress } from "@mui/material"; -interface RequireAuthProps { +interface PrivateRouteProps { children?: ReactNode; } -const ProtectedRoute = (props: RequireAuthProps) => { +const PrivateRoute = (props: PrivateRouteProps) => { const { children } = props; const { isLoggedIn, isLoading } = useContext(AuthContext); const location = useLocation(); @@ -27,4 +27,4 @@ const ProtectedRoute = (props: RequireAuthProps) => { return children; }; -export default ProtectedRoute; +export default PrivateRoute; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index dfc738df..024a0c97 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -3,13 +3,20 @@ 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 ProtectedRoute from "./components/common/ProtectedRoute"; import WorkspaceLayout from "./components/layouts/WorkspaceLayout"; +import GuestRoute from "./components/common/GuestRoute"; +import PrivateRoute from "./components/common/PrivateRoute"; + +const enum AccessType { + PRIVATE, // Authroized user can access only + PUBLIC, // Everyone can access + GUEST, // Not authorized user can access only +} const codePairRoutes = [ { path: "", - private: false, + accessType: AccessType.GUEST, element: , children: [ { @@ -20,18 +27,18 @@ const codePairRoutes = [ }, { path: "workspace", - private: true, + accessType: AccessType.PRIVATE, element: , children: [ { - path: "", + path: ":workspaceId", element: , }, ], }, { path: ":documentId", - private: true, + accessType: AccessType.PUBLIC, element: , children: [ { @@ -42,15 +49,17 @@ const codePairRoutes = [ }, { path: "auth/callback", - private: false, + accessType: AccessType.GUEST, element: , }, ]; const injectProtectedRoute = (routes: typeof codePairRoutes) => { return routes.map((route) => { - if (route.private) { - route.element = {route.element}; + if (route.accessType === AccessType.PRIVATE) { + route.element = {route.element}; + } else if (route.accessType === AccessType.GUEST) { + route.element = {route.element}; } return route; From 975b877137b97fba936bfb936b7b12c3f3c584ba Mon Sep 17 00:00:00 2001 From: devleejb Date: Fri, 19 Jan 2024 16:11:13 +0900 Subject: [PATCH 9/9] Add retrieving a user information --- .../components/drawers/WorkspaceDrawer.tsx | 48 +++++++++++++++++++ frontend/src/pages/workspace/Index.tsx | 4 +- frontend/src/routes.tsx | 5 +- 3 files changed, 53 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/drawers/WorkspaceDrawer.tsx 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/pages/workspace/Index.tsx b/frontend/src/pages/workspace/Index.tsx index 29529361..302ccd00 100644 --- a/frontend/src/pages/workspace/Index.tsx +++ b/frontend/src/pages/workspace/Index.tsx @@ -1,7 +1,7 @@ -import { Container } from "@mui/material"; +import WorkspaceDrawer from "../../components/drawers/WorkspaceDrawer"; function WorkspaceIndex() { - return ; + return ; } export default WorkspaceIndex; diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index 024a0c97..ff06a4bb 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -6,6 +6,7 @@ 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 @@ -28,11 +29,11 @@ const codePairRoutes = [ { path: "workspace", accessType: AccessType.PRIVATE, - element: , + element: , children: [ { path: ":workspaceId", - element: , + element: , }, ], },