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)
+ }
+ }
+)