From c1f1e2e22f4166087d8b5c18244fe0778e828a57 Mon Sep 17 00:00:00 2001 From: devleejb <devleejb@gmail.com> Date: Tue, 23 Jan 2024 15:47:52 +0900 Subject: [PATCH 1/4] Generate share url --- ...eate-workspace-document-share-token.dto.ts | 2 - .../src/components/common/ShareButton.tsx | 23 +++ .../src/components/headers/EditorHeader.tsx | 6 +- .../src/components/modals/MemberModal.tsx | 2 +- frontend/src/components/modals/ShareModal.tsx | 148 ++++++++++++++++++ frontend/src/hooks/api/types/document.d.ts | 1 + .../hooks/api/types/workspaceDocument.d.ts | 9 ++ frontend/src/hooks/api/workspaceDocument.ts | 15 ++ .../src/utils/{invitation.ts => expire.ts} | 0 frontend/src/utils/share.ts | 4 + 10 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/common/ShareButton.tsx create mode 100644 frontend/src/components/modals/ShareModal.tsx rename frontend/src/utils/{invitation.ts => expire.ts} (100%) create mode 100644 frontend/src/utils/share.ts diff --git a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts index dcc2a9a6..73cf60ed 100644 --- a/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts +++ b/backend/src/workspace-documents/dto/create-workspace-document-share-token.dto.ts @@ -1,6 +1,5 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsDate } from "class-validator"; import { ShareRoleEnum } from "src/utils/constants/share-role"; export class CreateWorkspaceDocumentShareTokenDto { @@ -9,6 +8,5 @@ export class CreateWorkspaceDocumentShareTokenDto { @ApiProperty({ type: Date, description: "Share link expiration date" }) @Type(() => Date) - @IsDate() expiredAt: Date; } diff --git a/frontend/src/components/common/ShareButton.tsx b/frontend/src/components/common/ShareButton.tsx new file mode 100644 index 00000000..6b5fbd48 --- /dev/null +++ b/frontend/src/components/common/ShareButton.tsx @@ -0,0 +1,23 @@ +import { IconButton } from "@mui/material"; +import ShareIcon from "@mui/icons-material/Share"; +import ShareModal from "../modals/ShareModal"; +import { useState } from "react"; + +function ShareButton() { + const [shareModalOpen, setShareModalOpen] = useState(false); + + const handleShareModalOpen = () => { + setShareModalOpen((prev) => !prev); + }; + + return ( + <> + <IconButton onClick={handleShareModalOpen}> + <ShareIcon /> + </IconButton> + <ShareModal open={shareModalOpen} onClose={handleShareModalOpen} /> + </> + ); +} + +export default ShareButton; diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx index 4b9c24d2..1719f461 100644 --- a/frontend/src/components/headers/EditorHeader.tsx +++ b/frontend/src/components/headers/EditorHeader.tsx @@ -17,6 +17,7 @@ import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice"; import ThemeButton from "../common/ThemeButton"; import { createDocumentKey } from "../../utils/document"; import { useNavigate } from "react-router-dom"; +import ShareButton from "../common/ShareButton"; function EditorHeader() { const dispatch = useDispatch(); @@ -66,7 +67,10 @@ function EditorHeader() { </IconButton> </Tooltip> </Stack> - <ThemeButton /> + <Stack direction="row" alignItems="center" gap={1}> + <ShareButton /> + <ThemeButton /> + </Stack> </Stack> </Toolbar> </AppBar> diff --git a/frontend/src/components/modals/MemberModal.tsx b/frontend/src/components/modals/MemberModal.tsx index 83f2dc47..2d68857c 100644 --- a/frontend/src/components/modals/MemberModal.tsx +++ b/frontend/src/components/modals/MemberModal.tsx @@ -22,7 +22,7 @@ import { useMemo, useState } from "react"; import { User } from "../../hooks/api/types/user"; import InfiniteScroll from "react-infinite-scroller"; import { FormContainer, SelectElement } from "react-hook-form-mui"; -import { invitationExpiredStringList } from "../../utils/invitation"; +import { invitationExpiredStringList } from "../../utils/expire"; import moment, { unitOfTime } from "moment"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import clipboard from "clipboardy"; diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx new file mode 100644 index 00000000..67574cd8 --- /dev/null +++ b/frontend/src/components/modals/ShareModal.tsx @@ -0,0 +1,148 @@ +import { + Button, + FormControl, + IconButton, + Modal, + ModalProps, + Paper, + Stack, + Tooltip, + Typography, +} from "@mui/material"; +import { FormContainer, SelectElement } from "react-hook-form-mui"; +import { invitationExpiredStringList } from "../../utils/expire"; +import { useState } from "react"; +import moment, { unitOfTime } from "moment"; +import { useParams } from "react-router"; +import { useGetDocumentQuery } from "../../hooks/api/document"; +import { useCreateWorkspaceSharingTokenMutation } from "../../hooks/api/workspaceDocument"; +import { ShareRole } from "../../utils/share"; +import clipboard from "clipboardy"; +import { useSnackbar } from "notistack"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CloseIcon from "@mui/icons-material/Close"; + +interface ShareModalProps extends Omit<ModalProps, "children"> {} + +function ShareModal(props: ShareModalProps) { + const { ...modalProps } = props; + const params = useParams(); + const [shareUrl, setShareUrl] = useState<string | null>(null); + const { data: document } = useGetDocumentQuery(params.documentSlug || ""); + const { mutateAsync: createWorkspaceSharingToken } = useCreateWorkspaceSharingTokenMutation( + document?.workspaceId || "", + document?.id || "" + ); + const { enqueueSnackbar } = useSnackbar(); + + const handleCreateShareUrl = async (data: { expiredString: string; role: ShareRole }) => { + let addedTime: Date | null; + + if (data.expiredString === invitationExpiredStringList[0]) { + addedTime = null; + } else { + const [num, unit] = data.expiredString.split(" "); + addedTime = moment() + .add(Number(num), unit as unitOfTime.DurationConstructor) + .toDate(); + } + + const { sharingToken } = await createWorkspaceSharingToken({ + role: data.role, + expiredAt: addedTime, + }); + + setShareUrl( + `${window.location.origin}/document/${params.documentSlug}?token=${sharingToken}` + ); + }; + + const handleCopyShareUrl = async () => { + if (!shareUrl) return; + + await clipboard.write(shareUrl); + enqueueSnackbar("URL Copied!", { variant: "success" }); + }; + + return ( + <Modal disableAutoFocus {...modalProps}> + <Paper + sx={{ + position: "absolute", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + p: 4, + width: 400, + }} + > + <IconButton + sx={{ + position: "absolute", + top: 28, + right: 28, + }} + onClick={(e) => props.onClose?.(e, "backdropClick")} + > + <CloseIcon /> + </IconButton> + <Stack gap={1}> + <Typography variant="subtitle1">Share Link</Typography> + <FormControl> + <FormContainer + defaultValues={{ + expiredString: invitationExpiredStringList[0], + role: Object.values(ShareRole)[0], + }} + onSuccess={handleCreateShareUrl} + > + <Stack gap={2}> + <SelectElement + label="Role" + name="role" + options={Object.values(ShareRole).map((role) => ({ + id: role, + label: role, + }))} + size="small" + sx={{ + width: 1, + }} + variant="filled" + /> + <SelectElement + label="Expired Date" + name="expiredString" + options={invitationExpiredStringList.map((expiredString) => ({ + id: expiredString, + label: expiredString, + }))} + size="small" + sx={{ + width: 1, + }} + variant="filled" + /> + <Button type="submit" variant="contained"> + Generate + </Button> + </Stack> + </FormContainer> + </FormControl> + {Boolean(shareUrl) && ( + <Stack direction="row" alignItems="center" gap={2}> + <Typography variant="body1">{shareUrl}</Typography> + <Tooltip title="Copy URL"> + <IconButton onClick={handleCopyShareUrl}> + <ContentCopyIcon /> + </IconButton> + </Tooltip> + </Stack> + )} + </Stack> + </Paper> + </Modal> + ); +} + +export default ShareModal; diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts index d746e103..7a29116e 100644 --- a/frontend/src/hooks/api/types/document.d.ts +++ b/frontend/src/hooks/api/types/document.d.ts @@ -1,5 +1,6 @@ export class Document { id: string; + workspaceId: string; yorkieDocumentId: string; title: string; slug: string; diff --git a/frontend/src/hooks/api/types/workspaceDocument.d.ts b/frontend/src/hooks/api/types/workspaceDocument.d.ts index ee35a854..05ae6db4 100644 --- a/frontend/src/hooks/api/types/workspaceDocument.d.ts +++ b/frontend/src/hooks/api/types/workspaceDocument.d.ts @@ -10,3 +10,12 @@ export class CreateDocumentRequest { } export class CreateDocumentResponse extends Document {} + +export class CreateDocumentShareTokenRequest { + role: ShareRole; + expiredAt: Date | null; +} + +export class CreateDocumentShareTokenResponse { + sharingToken: string; +} diff --git a/frontend/src/hooks/api/workspaceDocument.ts b/frontend/src/hooks/api/workspaceDocument.ts index bb81a02e..ad4da6fb 100644 --- a/frontend/src/hooks/api/workspaceDocument.ts +++ b/frontend/src/hooks/api/workspaceDocument.ts @@ -3,6 +3,8 @@ import axios from "axios"; import { CreateDocumentRequest, CreateDocumentResponse, + CreateDocumentShareTokenRequest, + CreateDocumentShareTokenResponse, GetWorkspaceDocumentListResponse, } from "./types/workspaceDocument"; @@ -53,3 +55,16 @@ export const useCreateDocumentMutation = (workspaceId: string) => { }, }); }; + +export const useCreateWorkspaceSharingTokenMutation = (workspaceId: string, documentId: string) => { + return useMutation({ + mutationFn: async (data: CreateDocumentShareTokenRequest) => { + const res = await axios.post<CreateDocumentShareTokenResponse>( + `/workspaces/${workspaceId}/documents/${documentId}/share-token`, + data + ); + + return res.data; + }, + }); +}; diff --git a/frontend/src/utils/invitation.ts b/frontend/src/utils/expire.ts similarity index 100% rename from frontend/src/utils/invitation.ts rename to frontend/src/utils/expire.ts diff --git a/frontend/src/utils/share.ts b/frontend/src/utils/share.ts new file mode 100644 index 00000000..27cfc72c --- /dev/null +++ b/frontend/src/utils/share.ts @@ -0,0 +1,4 @@ +export enum ShareRole { + READ = "READ", + EDIT = "EDIT", +} From 1b20758323f6240a1be2dfb3cd120cb7c58538e1 Mon Sep 17 00:00:00 2001 From: devleejb <devleejb@gmail.com> Date: Tue, 23 Jan 2024 16:23:18 +0900 Subject: [PATCH 2/4] Add document sharing --- .../src/components/common/ShareButton.tsx | 2 +- frontend/src/components/editor/Preview.tsx | 9 ++- .../src/components/headers/EditorHeader.tsx | 69 +++++++++---------- frontend/src/components/modals/ShareModal.tsx | 6 +- frontend/src/hooks/api/document.ts | 31 ++++++++- frontend/src/hooks/api/types/document.d.ts | 6 ++ frontend/src/pages/document/Index.tsx | 33 ++++++--- frontend/src/routes.tsx | 2 +- frontend/src/store/editorSlice.ts | 8 ++- 9 files changed, 111 insertions(+), 55 deletions(-) diff --git a/frontend/src/components/common/ShareButton.tsx b/frontend/src/components/common/ShareButton.tsx index 6b5fbd48..f0972232 100644 --- a/frontend/src/components/common/ShareButton.tsx +++ b/frontend/src/components/common/ShareButton.tsx @@ -12,7 +12,7 @@ function ShareButton() { return ( <> - <IconButton onClick={handleShareModalOpen}> + <IconButton onClick={handleShareModalOpen} color="inherit"> <ShareIcon /> </IconButton> <ShareModal open={shareModalOpen} onClose={handleShareModalOpen} /> diff --git a/frontend/src/components/editor/Preview.tsx b/frontend/src/components/editor/Preview.tsx index 8080f5a5..1003ab5b 100644 --- a/frontend/src/components/editor/Preview.tsx +++ b/frontend/src/components/editor/Preview.tsx @@ -2,7 +2,7 @@ import MarkdownPreview from "@uiw/react-markdown-preview"; import { useCurrentTheme } from "../../hooks/useCurrentTheme"; import { useSelector } from "react-redux"; import { selectEditor } from "../../store/editorSlice"; -import { CircularProgress } from "@mui/material"; +import { CircularProgress, Stack } from "@mui/material"; import { useEffect, useState } from "react"; import "./editor.css"; @@ -26,7 +26,12 @@ function Preview() { }; }, [editorStore.doc]); - if (!editorStore?.doc) return <CircularProgress sx={{ marginX: "auto", mt: 4 }} />; + if (!editorStore?.doc) + return ( + <Stack direction="row" justifyContent="center"> + <CircularProgress sx={{ mt: 2 }} /> + </Stack> + ); return ( <MarkdownPreview diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx index 1719f461..c8b10a9e 100644 --- a/frontend/src/components/headers/EditorHeader.tsx +++ b/frontend/src/components/headers/EditorHeader.tsx @@ -1,6 +1,5 @@ import { AppBar, - IconButton, Paper, Stack, ToggleButton, @@ -11,64 +10,60 @@ import { import EditIcon from "@mui/icons-material/Edit"; import VerticalSplitIcon from "@mui/icons-material/VerticalSplit"; import VisibilityIcon from "@mui/icons-material/Visibility"; -import AddIcon from "@mui/icons-material/Add"; import { useDispatch, useSelector } from "react-redux"; import { EditorModeType, selectEditor, setMode } from "../../store/editorSlice"; import ThemeButton from "../common/ThemeButton"; -import { createDocumentKey } from "../../utils/document"; -import { useNavigate } from "react-router-dom"; import ShareButton from "../common/ShareButton"; +import { useEffect } from "react"; function EditorHeader() { const dispatch = useDispatch(); const editorState = useSelector(selectEditor); - const navigate = useNavigate(); + + useEffect(() => { + if (editorState.shareRole === "READ") { + dispatch(setMode("read")); + } + }, [editorState.shareRole]); const handleChangeMode = (newMode: EditorModeType) => { dispatch(setMode(newMode)); }; - const handleCreateNewDocument = () => { - navigate(`/${createDocumentKey()}`); - }; - return ( <AppBar position="static" sx={{ zIndex: 100 }}> <Toolbar> <Stack width="100%" direction="row" justifyContent="space-between"> <Stack direction="row" spacing={1}> <Paper> - <ToggleButtonGroup - value={editorState.mode} - exclusive - onChange={(_, newMode) => handleChangeMode(newMode)} - size="small" - > - <ToggleButton value="edit" aria-label="edit"> - <Tooltip title="Edit Mode"> - <EditIcon /> - </Tooltip> - </ToggleButton> - <ToggleButton value="both" aria-label="both"> - <Tooltip title="Both Mode"> - <VerticalSplitIcon /> - </Tooltip> - </ToggleButton> - <ToggleButton value="read" aria-label="read"> - <Tooltip title="Read Mode"> - <VisibilityIcon /> - </Tooltip> - </ToggleButton> - </ToggleButtonGroup> + {editorState.shareRole !== "READ" && ( + <ToggleButtonGroup + value={editorState.mode} + exclusive + onChange={(_, newMode) => handleChangeMode(newMode)} + size="small" + > + <ToggleButton value="edit" aria-label="edit"> + <Tooltip title="Edit Mode"> + <EditIcon /> + </Tooltip> + </ToggleButton> + <ToggleButton value="both" aria-label="both"> + <Tooltip title="Both Mode"> + <VerticalSplitIcon /> + </Tooltip> + </ToggleButton> + <ToggleButton value="read" aria-label="read"> + <Tooltip title="Read Mode"> + <VisibilityIcon /> + </Tooltip> + </ToggleButton> + </ToggleButtonGroup> + )} </Paper> - <Tooltip title="Create New Note"> - <IconButton color="inherit" onClick={handleCreateNewDocument}> - <AddIcon /> - </IconButton> - </Tooltip> </Stack> <Stack direction="row" alignItems="center" gap={1}> - <ShareButton /> + {!editorState.shareRole && <ShareButton />} <ThemeButton /> </Stack> </Stack> diff --git a/frontend/src/components/modals/ShareModal.tsx b/frontend/src/components/modals/ShareModal.tsx index 67574cd8..87afda76 100644 --- a/frontend/src/components/modals/ShareModal.tsx +++ b/frontend/src/components/modals/ShareModal.tsx @@ -21,14 +21,18 @@ import clipboard from "clipboardy"; import { useSnackbar } from "notistack"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import CloseIcon from "@mui/icons-material/Close"; +import { useSearchParams } from "react-router-dom"; interface ShareModalProps extends Omit<ModalProps, "children"> {} function ShareModal(props: ShareModalProps) { const { ...modalProps } = props; const params = useParams(); + const [searchParams] = useSearchParams(); const [shareUrl, setShareUrl] = useState<string | null>(null); - const { data: document } = useGetDocumentQuery(params.documentSlug || ""); + const { data: document } = useGetDocumentQuery( + searchParams.get("token") ? null : params.documentSlug + ); const { mutateAsync: createWorkspaceSharingToken } = useCreateWorkspaceSharingTokenMutation( document?.workspaceId || "", document?.id || "" diff --git a/frontend/src/hooks/api/document.ts b/frontend/src/hooks/api/document.ts index f67faa0e..ce2dd088 100644 --- a/frontend/src/hooks/api/document.ts +++ b/frontend/src/hooks/api/document.ts @@ -1,21 +1,46 @@ import { useQuery } from "@tanstack/react-query"; import axios from "axios"; -import { GetDocumentResponse } from "./types/document"; +import { GetDocumentBySharingTokenResponse, GetDocumentResponse } from "./types/document"; export const generateGetDocumentQueryKey = (documentSlug: string) => { return ["documents", documentSlug]; }; -export const useGetDocumentQuery = (documentSlug: string) => { +export const generateGetDocumentBySharingTokenQueryKey = (sharingToken: string) => { + return ["documents", "share", sharingToken]; +}; + +export const useGetDocumentQuery = (documentSlug?: string | null) => { const query = useQuery({ queryKey: generateGetDocumentQueryKey(documentSlug || ""), enabled: Boolean(documentSlug), queryFn: async () => { + console.log("zz", documentSlug); const res = await axios.get<GetDocumentResponse>(`/documents/${documentSlug}`); return res.data; }, meta: { - errorMessage: "This is a non-existent or unauthorized Workspace.", + errorMessage: "This is a non-existent or unauthorized document.", + }, + }); + + return query; +}; + +export const useGetDocumentBySharingTokenQuery = (sharingToken?: string | null) => { + const query = useQuery({ + queryKey: generateGetDocumentQueryKey(sharingToken || ""), + enabled: Boolean(sharingToken), + queryFn: async () => { + const res = await axios.get<GetDocumentBySharingTokenResponse>("/documents/share", { + params: { + token: sharingToken, + }, + }); + return res.data; + }, + meta: { + errorMessage: "This is a non-existent or expired document.", }, }); diff --git a/frontend/src/hooks/api/types/document.d.ts b/frontend/src/hooks/api/types/document.d.ts index 7a29116e..e7a594ba 100644 --- a/frontend/src/hooks/api/types/document.d.ts +++ b/frontend/src/hooks/api/types/document.d.ts @@ -1,3 +1,5 @@ +import { ShareRole } from "../../../utils/share"; + export class Document { id: string; workspaceId: string; @@ -10,3 +12,7 @@ export class Document { } export class GetDocumentResponse extends Document {} + +export class GetDocumentBySharingTokenResponse extends Document { + role: ShareRole; +} diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 4dbd7cb2..804d7160 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -1,7 +1,7 @@ -import { useEffect } from "react"; +import { useContext, useEffect } from "react"; import Editor from "../../components/editor/Editor"; import * as yorkie from "yorkie-js-sdk"; -import { selectEditor, setClient, setDoc } from "../../store/editorSlice"; +import { selectEditor, setClient, setDoc, setShareRole } from "../../store/editorSlice"; import { useDispatch, useSelector } from "react-redux"; import { YorkieCodeMirrorDocType, @@ -13,21 +13,26 @@ import { Box, Paper } from "@mui/material"; import Resizable from "react-resizable-layout"; import { useWindowWidth } from "@react-hook/window-size"; import Preview from "../../components/editor/Preview"; -import { useParams } from "react-router-dom"; -import { useGetDocumentQuery } from "../../hooks/api/document"; +import { useParams, useSearchParams } from "react-router-dom"; +import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../hooks/api/document"; +import { AuthContext } from "../../contexts/AuthContext"; function EditorIndex() { - const params = useParams(); const dispatch = useDispatch(); + const params = useParams(); + const { isLoggedIn } = useContext(AuthContext); + const [searchParams] = useSearchParams(); const windowWidth = useWindowWidth(); const editorStore = useSelector(selectEditor); - const { data: document } = useGetDocumentQuery(params.documentSlug || ""); + const { data: document } = useGetDocumentQuery(isLoggedIn ? params.documentSlug : null); + const { data: sharedDocument } = useGetDocumentBySharingTokenQuery(searchParams.get("token")); useEffect(() => { let client: yorkie.Client; let doc: yorkie.Document<YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType>; + const yorkieDocuentId = document?.yorkieDocumentId || sharedDocument?.yorkieDocumentId; - if (!document?.yorkieDocumentId) return; + if (!yorkieDocuentId) return; const initializeYorkie = async () => { client = new yorkie.Client(import.meta.env.VITE_YORKIE_API_ADDR, { @@ -35,7 +40,7 @@ function EditorIndex() { }); await client.activate(); - doc = new yorkie.Document(document?.yorkieDocumentId as string); + doc = new yorkie.Document(yorkieDocuentId as string); await client.attach(doc, { initialPresence: { @@ -58,7 +63,17 @@ function EditorIndex() { cleanUp(); }; - }, [dispatch, document?.yorkieDocumentId]); + }, [dispatch, document?.yorkieDocumentId, sharedDocument?.yorkieDocumentId]); + + useEffect(() => { + if (!sharedDocument) return; + + dispatch(setShareRole(sharedDocument.role)); + + return () => { + setShareRole(null); + }; + }, [dispatch, sharedDocument, sharedDocument?.role]); return ( <Box height="calc(100% - 64px)"> diff --git a/frontend/src/routes.tsx b/frontend/src/routes.tsx index de28305a..310f5c99 100644 --- a/frontend/src/routes.tsx +++ b/frontend/src/routes.tsx @@ -52,7 +52,7 @@ const codePairRoutes: Array<CodePairRoute> = [ }, { path: "document", - accessType: AccessType.PRIVATE, + accessType: AccessType.PUBLIC, element: <EditorLayout />, children: [ { diff --git a/frontend/src/store/editorSlice.ts b/frontend/src/store/editorSlice.ts index 287be431..659120d1 100644 --- a/frontend/src/store/editorSlice.ts +++ b/frontend/src/store/editorSlice.ts @@ -3,6 +3,7 @@ import type { PayloadAction } from "@reduxjs/toolkit"; import { RootState } from "./store"; import * as yorkie from "yorkie-js-sdk"; import { YorkieCodeMirrorDocType, YorkieCodeMirrorPresenceType } from "../utils/yorkie/yorkieSync"; +import { ShareRole } from "../utils/share"; export type EditorModeType = "edit" | "both" | "read"; export type CodePairDocType = yorkie.Document< @@ -12,12 +13,14 @@ export type CodePairDocType = yorkie.Document< export interface EditorState { mode: EditorModeType; + shareRole: ShareRole | null; doc: CodePairDocType | null; client: yorkie.Client | null; } const initialState: EditorState = { mode: "both", + shareRole: null, doc: null, client: null, }; @@ -29,6 +32,9 @@ export const editorSlice = createSlice({ setMode: (state, action: PayloadAction<EditorModeType>) => { state.mode = action.payload; }, + setShareRole: (state, action: PayloadAction<ShareRole | null>) => { + state.shareRole = action.payload; + }, setDoc: (state, action: PayloadAction<CodePairDocType | null>) => { state.doc = action.payload; }, @@ -38,7 +44,7 @@ export const editorSlice = createSlice({ }, }); -export const { setMode, setDoc, setClient } = editorSlice.actions; +export const { setMode, setDoc, setClient, setShareRole } = editorSlice.actions; export const selectEditor = (state: RootState) => state.editor; From 872a02f3339ec4e5a74534f808c7fce8cae9803b Mon Sep 17 00:00:00 2001 From: devleejb <devleejb@gmail.com> Date: Tue, 23 Jan 2024 16:26:00 +0900 Subject: [PATCH 3/4] Add permission checking to note page --- frontend/src/hooks/api/document.ts | 1 - frontend/src/pages/document/Index.tsx | 12 +++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/frontend/src/hooks/api/document.ts b/frontend/src/hooks/api/document.ts index ce2dd088..1a3512d4 100644 --- a/frontend/src/hooks/api/document.ts +++ b/frontend/src/hooks/api/document.ts @@ -15,7 +15,6 @@ export const useGetDocumentQuery = (documentSlug?: string | null) => { queryKey: generateGetDocumentQueryKey(documentSlug || ""), enabled: Boolean(documentSlug), queryFn: async () => { - console.log("zz", documentSlug); const res = await axios.get<GetDocumentResponse>(`/documents/${documentSlug}`); return res.data; }, diff --git a/frontend/src/pages/document/Index.tsx b/frontend/src/pages/document/Index.tsx index 804d7160..517043a2 100644 --- a/frontend/src/pages/document/Index.tsx +++ b/frontend/src/pages/document/Index.tsx @@ -13,7 +13,7 @@ import { Box, Paper } from "@mui/material"; import Resizable from "react-resizable-layout"; import { useWindowWidth } from "@react-hook/window-size"; import Preview from "../../components/editor/Preview"; -import { useParams, useSearchParams } from "react-router-dom"; +import { Navigate, useParams, useSearchParams } from "react-router-dom"; import { useGetDocumentBySharingTokenQuery, useGetDocumentQuery } from "../../hooks/api/document"; import { AuthContext } from "../../contexts/AuthContext"; @@ -24,8 +24,11 @@ function EditorIndex() { const [searchParams] = useSearchParams(); const windowWidth = useWindowWidth(); const editorStore = useSelector(selectEditor); - const { data: document } = useGetDocumentQuery(isLoggedIn ? params.documentSlug : null); - const { data: sharedDocument } = useGetDocumentBySharingTokenQuery(searchParams.get("token")); + const { data: document, isError: isDocumentError } = useGetDocumentQuery( + isLoggedIn ? params.documentSlug : null + ); + const { data: sharedDocument, isError: isSharedDocumentError } = + useGetDocumentBySharingTokenQuery(searchParams.get("token")); useEffect(() => { let client: yorkie.Client; @@ -75,6 +78,9 @@ function EditorIndex() { }; }, [dispatch, sharedDocument, sharedDocument?.role]); + if (isDocumentError || isSharedDocumentError) + return <Navigate to="/" state={{ from: location }} replace />; + return ( <Box height="calc(100% - 64px)"> {/* For Markdown Preview Theme */} From 845f6d71029314cfbc81f20df2dc23b2164be25b Mon Sep 17 00:00:00 2001 From: devleejb <devleejb@gmail.com> Date: Tue, 23 Jan 2024 16:33:06 +0900 Subject: [PATCH 4/4] Fix lint --- frontend/src/components/headers/EditorHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/headers/EditorHeader.tsx b/frontend/src/components/headers/EditorHeader.tsx index c8b10a9e..cf73698f 100644 --- a/frontend/src/components/headers/EditorHeader.tsx +++ b/frontend/src/components/headers/EditorHeader.tsx @@ -24,7 +24,7 @@ function EditorHeader() { if (editorState.shareRole === "READ") { dispatch(setMode("read")); } - }, [editorState.shareRole]); + }, [dispatch, editorState.shareRole]); const handleChangeMode = (newMode: EditorModeType) => { dispatch(setMode(newMode));