diff --git a/client/components/new-post/password-modal/index.tsx b/client/components/new-post/password-modal/index.tsx index 93ad0cfc..379b09cc 100644 --- a/client/components/new-post/password-modal/index.tsx +++ b/client/components/new-post/password-modal/index.tsx @@ -37,7 +37,7 @@ const PasswordModal = ({ {/* TODO: investigate disableBackdropClick not updating state? */} { - + Enter a password {!error && creating && ( diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index ff4f8bf0..8999ae83 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -17,19 +17,23 @@ import ExpirationBadge from "@components/badges/expiration-badge" 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" type Props = { post: Post + isProtected?: boolean } -const PostPage = ({ post }: Props) => { - const router = useRouter() - - const isMobile = useMediaQuery("mobile") +const PostPage = ({ post: initialPost, isProtected }: Props) => { + const [post, setPost] = useState(initialPost) const [isExpired, setIsExpired] = useState( post.expiresAt ? new Date(post.expiresAt) < new Date() : null ) const [isLoading, setIsLoading] = useState(true) + + const router = useRouter() + const isMobile = useMediaQuery("mobile") + useEffect(() => { const isOwner = post.users ? post.users[0].id === Cookies.get("drift-userid") @@ -84,6 +88,8 @@ const PostPage = ({ post }: Props) => { return <> } + const isAvailable = !isExpired && !isProtected && post.title + return ( { description={post.description} isPrivate={false} /> - + {!isAvailable && }
diff --git a/client/components/post-page/password-modal-wrapper.tsx b/client/components/post-page/password-modal-wrapper.tsx new file mode 100644 index 00000000..3a384b3f --- /dev/null +++ b/client/components/post-page/password-modal-wrapper.tsx @@ -0,0 +1,64 @@ +import PasswordModal from "@components/new-post/password-modal" +import { Page, useToasts } from "@geist-ui/core" +import { Post } from "@lib/types" +import { useRouter } from "next/router" +import { useState } from "react" + +type Props = { + setPost: (post: Post) => void +} + +const PasswordModalPage = ({ setPost }: Props) => { + const router = useRouter() + const { setToast } = useToasts() + const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(true) + + const onSubmit = async (password: string) => { + const res = await fetch( + `/server-api/posts/authenticate?id=${router.query.id}&password=${password}`, + { + method: "GET", + headers: { + "Content-Type": "application/json" + } + } + ) + + if (!res.ok) { + setToast({ + type: "error", + text: "Wrong password" + }) + return + } + + const data = await res.json() + if (data) { + if (data.error) { + setToast({ + text: data.error, + type: "error" + }) + } else { + setIsPasswordModalOpen(false) + setPost(data) + } + } + } + + const onClose = () => { + setIsPasswordModalOpen(false) + router.push("/") + } + + return ( + + ) +} + +export default PasswordModalPage \ No newline at end of file diff --git a/client/lib/get-post-path.ts b/client/lib/get-post-path.ts index 76cac64c..7950e517 100644 --- a/client/lib/get-post-path.ts +++ b/client/lib/get-post-path.ts @@ -3,9 +3,9 @@ import type { PostVisibility } from "./types" export default function getPostPath(visibility: PostVisibility, id: string) { switch (visibility) { case "private": - return `/post/private/${id}` + // return `/post/private/${id}` case "protected": - return `/post/protected/${id}` + // return `/post/protected/${id}` case "unlisted": case "public": return `/post/${id}` diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx index 08c60c98..659044e0 100644 --- a/client/pages/post/[id].tsx +++ b/client/pages/post/[id].tsx @@ -5,31 +5,37 @@ import PostPage from "@components/post-page" export type PostProps = { post: Post + isProtected?: boolean } -const PostView = ({ post }: PostProps) => { - return +const PostView = ({ post, isProtected }: PostProps) => { + return } export const getServerSideProps: GetServerSideProps = async ({ params, + req, res }) => { const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, { method: "GET", headers: { "Content-Type": "application/json", - "x-secret-key": process.env.SECRET_KEY || "" + "x-secret-key": process.env.SECRET_KEY || "", + Authorization: `Bearer ${req.cookies["drift-token"]}` } }) - const sMaxAge = 60 * 60 * 24 - res.setHeader( - "Cache-Control", - `public, s-maxage=${sMaxAge}, max-age=${sMaxAge}` - ) - - if (!post.ok || post.status !== 200) { + if (post.status === 401 || post.status === 403) { + return { + // can't access the post if it's private + redirect: { + destination: "/", + permanent: false + }, + props: {} + } + } else if (post.status === 404 || !post.ok) { return { redirect: { destination: "/404", @@ -39,7 +45,27 @@ export const getServerSideProps: GetServerSideProps = async ({ } } - const json = await post.json() + const json = await post.json() as Post + const isAuthor = json.users?.find(user => user.id === req.cookies["drift-userid"]) + + if (json.visibility === "public" || json.visibility === "unlisted") { + const sMaxAge = 60 * 60 * 12 // half a day + res.setHeader( + "Cache-Control", + `public, s-maxage=${sMaxAge}, max-age=${sMaxAge}` + ) + } else if (json.visibility === "protected" && !isAuthor) { + return { + props: { + post: { + id: json.id, + visibility: json.visibility, + expiresAt: json.expiresAt, + }, + isProtected: true + } + } + } return { props: { diff --git a/client/pages/post/private/[id].tsx b/client/pages/post/private/[id].tsx deleted file mode 100644 index 8b6feca2..00000000 --- a/client/pages/post/private/[id].tsx +++ /dev/null @@ -1,60 +0,0 @@ -import cookie from "cookie" -import type { GetServerSideProps } from "next" -import { Post } from "@lib/types" -import PostPage from "@components/post-page" - -export type PostProps = { - post: Post -} - -const Post = ({ post }: PostProps) => { - return -} - -export const getServerSideProps: GetServerSideProps = async (context) => { - const headers = context.req.headers - const host = headers.host - const driftToken = cookie.parse(headers.cookie || "")[`drift-token`] - - if (context.query.id) { - const post = await fetch( - "http://" + host + `/server-api/posts/${context.query.id}`, - { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${driftToken}`, - "x-secret-key": process.env.SECRET_KEY || "" - } - } - ) - - if (!post.ok || post.status !== 200) { - return { - redirect: { - destination: "/", - permanent: false - } - } - } - try { - const json = await post.json() - - return { - props: { - post: json - } - } - } catch (e) { - console.log(e) - } - } - - return { - props: { - post: null - } - } -} - -export default Post diff --git a/server/src/routes/posts.ts b/server/src/routes/posts.ts index a6eded17..19bc0776 100644 --- a/server/src/routes/posts.ts +++ b/server/src/routes/posts.ts @@ -238,8 +238,78 @@ posts.get( } ) +const fullPostSequelizeOptions = { + include: [ + { + model: File, + as: "files", + attributes: [ + "id", + "title", + "content", + "sha", + "createdAt", + "updatedAt" + ] + }, + { + model: User, + as: "users", + attributes: ["id", "username"] + }, + { + model: Post, + as: "parent", + attributes: ["id", "title", "visibility", "createdAt"] + } + ], + attributes: [ + "id", + "title", + "visibility", + "createdAt", + "updatedAt", + "deletedAt", + "expiresAt", + ] +} + +posts.get("/authenticate", + celebrate({ + query: { + id: Joi.string().required(), + password: Joi.string().required() + } + }), + async (req, res, next) => { + const { id, password } = req.query + + const post = await Post.findByPk(id?.toString(), { + ...fullPostSequelizeOptions, + attributes: [ + ...fullPostSequelizeOptions.attributes, + "password" + ] + }) + + const hash = crypto + .createHash("sha256") + .update(password?.toString() || "") + .digest("hex") + .toString() + + if (hash !== post?.password) { + return res.status(400).json({ error: "Incorrect password." }) + } + + res.json(post) + } +) + + posts.get( "/:id", + secretKey, celebrate({ params: { id: Joi.string().required() @@ -254,42 +324,7 @@ posts.get( } try { - const post = await Post.findByPk(req.params.id, { - include: [ - { - model: File, - as: "files", - attributes: [ - "id", - "title", - "content", - "sha", - "createdAt", - "updatedAt" - ] - }, - { - model: User, - as: "users", - attributes: ["id", "username"] - }, - { - model: Post, - as: "parent", - attributes: ["id", "title", "visibility", "createdAt"] - } - ], - attributes: [ - "id", - "title", - "visibility", - "createdAt", - "updatedAt", - "deletedAt", - "expiresAt", - "password" - ] - }) + const post = await Post.findByPk(req.params.id, fullPostSequelizeOptions) if (!post) { return res.status(404).json({ error: "Post not found" }) @@ -301,9 +336,7 @@ posts.get( } if (post.visibility === "public" || post?.visibility === "unlisted") { - secretKey(req, res, () => { - res.json(post) - }) + res.json(post) } else if (post.visibility === "private") { jwt(req as UserJwtRequest, res, () => { if (isUserAuthor(post)) { @@ -313,27 +346,8 @@ posts.get( } }) } else if (post.visibility === "protected") { - const { password } = req.query - if (!password || typeof password !== "string") { - return jwt(req as UserJwtRequest, res, () => { - if (isUserAuthor(post)) { - res.json(post) - } else { - res.status(403).send() - } - }) - } - - const hash = crypto - .createHash("sha256") - .update(password) - .digest("hex") - .toString() - - if (hash !== post.password) { - return res.status(400).json({ error: "Incorrect password." }) - } - + // The client ensures to not send the post to the client. + // See client/pages/post/[id].tsx::getServerSideProps res.json(post) } } catch (e) {