From b9d26e16f79e612d502b6482c31e0342aa3af82b Mon Sep 17 00:00:00 2001 From: "Joaquin \"Florius\" Azcarate" Date: Thu, 14 Apr 2022 23:27:38 +0200 Subject: [PATCH] Change post visibility (#83) * Change post visibility Closes #64 * Fix imports + right align controls --- .../badges/visibility-control/index.tsx | 111 ++++++++++++++++++ client/components/post-page/index.tsx | 17 ++- .../components/post-page/post-page.module.css | 5 + server/src/routes/posts.ts | 65 ++++++++++ 4 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 client/components/badges/visibility-control/index.tsx diff --git a/client/components/badges/visibility-control/index.tsx b/client/components/badges/visibility-control/index.tsx new file mode 100644 index 00000000..ec6c9f67 --- /dev/null +++ b/client/components/badges/visibility-control/index.tsx @@ -0,0 +1,111 @@ +import PasswordModal from "@components/new-post/password-modal" +import { Button, ButtonGroup, Loading, useToasts } from "@geist-ui/core" +import type { PostVisibility } from "@lib/types" +import Cookies from "js-cookie" +import { useCallback, useState } from "react" + +type Props = { + postId: string + visibility: PostVisibility + setVisibility: (visibility: PostVisibility) => void +} + +const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => { + const [isSubmitting, setSubmitting] = useState(false) + const [passwordModalVisible, setPasswordModalVisible] = useState(false) + const { setToast } = useToasts() + + const sendRequest = useCallback( + async (visibility: PostVisibility, password?: string) => { + const res = await fetch(`/server-api/posts/${postId}`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${Cookies.get("drift-token")}` + }, + body: JSON.stringify({ visibility, password }) + }) + + if (res.ok) { + const json = await res.json() + setVisibility(json.visibility) + } else { + const json = await res.json() + setToast({ + text: json.error.message, + type: "error" + }) + setPasswordModalVisible(false) + } + }, + [setToast] + ) + + const onSubmit = useCallback( + async (visibility: PostVisibility, password?: string) => { + if (visibility === "protected" && !password) { + setPasswordModalVisible(true) + return + } + setPasswordModalVisible(false) + const timeout = setTimeout(() => setSubmitting(true), 100) + + await sendRequest(visibility, password) + clearTimeout(timeout) + setSubmitting(false) + }, + [sendRequest] + ) + + const onClosePasswordModal = () => { + setPasswordModalVisible(false) + setSubmitting(false) + } + + const submitPassword = useCallback( + (password) => onSubmit("protected", password), + [onSubmit] + ) + + return ( + <> + {isSubmitting ? ( + + ) : ( + + + + + + + )} + + + ) +} + +export default VisibilityControl diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index 712d554d..e9ec19ef 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -4,7 +4,7 @@ import DocumentComponent from "@components/view-document" import styles from "./post-page.module.css" import homeStyles from "@styles/Home.module.css" -import type { File, Post } from "@lib/types" +import type { File, Post, PostVisibility } from "@lib/types" import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core" import { useEffect, useState } from "react" import Archive from "@geist-ui/icons/archive" @@ -18,6 +18,7 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge" import Cookies from "js-cookie" import getPostPath from "@lib/get-post-path" import PasswordModalPage from "./password-modal-wrapper" +import VisibilityControl from "@components/badges/visibility-control" type Props = { post: Post @@ -26,18 +27,19 @@ type Props = { const PostPage = ({ post: initialPost, isProtected }: Props) => { const [post, setPost] = useState(initialPost) + const [visibility, setVisibility] = useState(post.visibility) const [isExpired, setIsExpired] = useState( post.expiresAt ? new Date(post.expiresAt) < new Date() : null ) const [isLoading, setIsLoading] = useState(true) + const [isOwner] = useState( + post.users ? post.users[0].id === Cookies.get("drift-userid") : false + ) const router = useRouter() const isMobile = useMediaQuery("mobile") useEffect(() => { - const isOwner = post.users - ? post.users[0].id === Cookies.get("drift-userid") - : false if (!isOwner && isExpired) { router.push("/expired") } @@ -59,7 +61,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => { return () => { if (interval) clearInterval(interval) } - }, [isExpired, post.expiresAt, post.users, router]) + }, [isExpired, isOwner, post.expiresAt, post.users, router]) const download = async () => { if (!post.files) return @@ -143,7 +145,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => { {post.title} - + @@ -164,6 +166,9 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => { content={content} /> ))} + {isOwner && + + } diff --git a/client/components/post-page/post-page.module.css b/client/components/post-page/post-page.module.css index 181ada40..d92a920a 100644 --- a/client/components/post-page/post-page.module.css +++ b/client/components/post-page/post-page.module.css @@ -22,6 +22,11 @@ margin-bottom: var(--gap); } +.controls { + display: flex; + justify-content: flex-end; +} + @media screen and (max-width: 900px) { .header { flex-direction: column; diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index 36966bcf..6de9b281 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -402,3 +402,68 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => { next(e) } }) + + +posts.put( + "/:id", + jwt, + celebrate({ + params: { + id: Joi.string().required() + }, + body: { + visibility: Joi.string() + .custom(postVisibilitySchema, "valid visibility") + .required(), + password: Joi.string().optional(), + } + }), + async (req: UserJwtRequest, res, next) => { + try { + const isUserAuthor = (post: Post) => { + return ( + req.user?.id && + post.users?.map((user) => user.id).includes(req.user?.id) + ) + } + + const { visibility, password } = req.body; + + let hashedPassword: string = "" + if (visibility === "protected") { + hashedPassword = crypto + .createHash("sha256") + .update(password) + .digest("hex") + } + + const { id } = req.params; + const post = await Post.findByPk(id, { + include: [ + { + model: User, + as: "users", + attributes: ["id"] + }, + ] + }) + + if (!post) { + return res.status(404).json({ error: "Post not found" }) + } + + if (!isUserAuthor(post)) { + return res.status(403).json({ error: "This post does not belong to you" }) + } + + await Post.update( + { password: hashedPassword, visibility }, + { where: { id } } + ) + + res.json({ id, visibility }) + } catch (e) { + res.status(400).json(e) + } + } +)