client: remove need for multiple post page URLs

This commit is contained in:
Max Leiter 2022-04-12 16:48:12 -07:00
parent 4bcf791c86
commit 67e1b9889b
No known key found for this signature in database
GPG key ID: A3512F2F2F17EBDA
7 changed files with 189 additions and 139 deletions

View file

@ -37,7 +37,7 @@ const PasswordModal = ({
{/* TODO: investigate disableBackdropClick not updating state? */} {/* TODO: investigate disableBackdropClick not updating state? */}
{ {
<Modal visible={isOpen} disableBackdropClick={true}> <Modal visible={isOpen} disableBackdropClick={false}>
<Modal.Title>Enter a password</Modal.Title> <Modal.Title>Enter a password</Modal.Title>
<Modal.Content> <Modal.Content>
{!error && creating && ( {!error && creating && (

View file

@ -17,19 +17,23 @@ import ExpirationBadge from "@components/badges/expiration-badge"
import CreatedAgoBadge from "@components/badges/created-ago-badge" import CreatedAgoBadge from "@components/badges/created-ago-badge"
import Cookies from "js-cookie" import Cookies from "js-cookie"
import getPostPath from "@lib/get-post-path" import getPostPath from "@lib/get-post-path"
import PasswordModalPage from "./password-modal-wrapper"
type Props = { type Props = {
post: Post post: Post
isProtected?: boolean
} }
const PostPage = ({ post }: Props) => { const PostPage = ({ post: initialPost, isProtected }: Props) => {
const router = useRouter() const [post, setPost] = useState<Post>(initialPost)
const isMobile = useMediaQuery("mobile")
const [isExpired, setIsExpired] = useState( const [isExpired, setIsExpired] = useState(
post.expiresAt ? new Date(post.expiresAt) < new Date() : null post.expiresAt ? new Date(post.expiresAt) < new Date() : null
) )
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
const router = useRouter()
const isMobile = useMediaQuery("mobile")
useEffect(() => { useEffect(() => {
const isOwner = post.users const isOwner = post.users
? post.users[0].id === Cookies.get("drift-userid") ? post.users[0].id === Cookies.get("drift-userid")
@ -84,6 +88,8 @@ const PostPage = ({ post }: Props) => {
return <></> return <></>
} }
const isAvailable = !isExpired && !isProtected && post.title
return ( return (
<Page width={"100%"}> <Page width={"100%"}>
<PageSeo <PageSeo
@ -91,7 +97,7 @@ const PostPage = ({ post }: Props) => {
description={post.description} description={post.description}
isPrivate={false} isPrivate={false}
/> />
{!isAvailable && <PasswordModalPage setPost={setPost} />}
<Page.Content className={homeStyles.main}> <Page.Content className={homeStyles.main}>
<div className={styles.header}> <div className={styles.header}>
<span className={styles.buttons}> <span className={styles.buttons}>

View file

@ -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 (
<PasswordModal
creating={false}
onClose={onClose}
onSubmit={onSubmit}
isOpen={isPasswordModalOpen}
/>
)
}
export default PasswordModalPage

View file

@ -3,9 +3,9 @@ import type { PostVisibility } from "./types"
export default function getPostPath(visibility: PostVisibility, id: string) { export default function getPostPath(visibility: PostVisibility, id: string) {
switch (visibility) { switch (visibility) {
case "private": case "private":
return `/post/private/${id}` // return `/post/private/${id}`
case "protected": case "protected":
return `/post/protected/${id}` // return `/post/protected/${id}`
case "unlisted": case "unlisted":
case "public": case "public":
return `/post/${id}` return `/post/${id}`

View file

@ -5,31 +5,37 @@ import PostPage from "@components/post-page"
export type PostProps = { export type PostProps = {
post: Post post: Post
isProtected?: boolean
} }
const PostView = ({ post }: PostProps) => { const PostView = ({ post, isProtected }: PostProps) => {
return <PostPage post={post} /> return <PostPage isProtected={isProtected} post={post} />
} }
export const getServerSideProps: GetServerSideProps = async ({ export const getServerSideProps: GetServerSideProps = async ({
params, params,
req,
res res
}) => { }) => {
const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, { const post = await fetch(process.env.API_URL + `/posts/${params?.id}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "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 if (post.status === 401 || post.status === 403) {
res.setHeader( return {
"Cache-Control", // can't access the post if it's private
`public, s-maxage=${sMaxAge}, max-age=${sMaxAge}` redirect: {
) destination: "/",
permanent: false
if (!post.ok || post.status !== 200) { },
props: {}
}
} else if (post.status === 404 || !post.ok) {
return { return {
redirect: { redirect: {
destination: "/404", 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 { return {
props: { props: {

View file

@ -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 <PostPage post={post} />
}
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

View file

@ -238,23 +238,7 @@ posts.get(
} }
) )
posts.get( const fullPostSequelizeOptions = {
"/:id",
celebrate({
params: {
id: Joi.string().required()
}
}),
async (req: UserJwtRequest, res, next) => {
const isUserAuthor = (post: Post) => {
return (
req.user?.id &&
post.users?.map((user) => user.id).includes(req.user?.id)
)
}
try {
const post = await Post.findByPk(req.params.id, {
include: [ include: [
{ {
model: File, model: File,
@ -287,10 +271,61 @@ posts.get(
"updatedAt", "updatedAt",
"deletedAt", "deletedAt",
"expiresAt", "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" "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()
}
}),
async (req: UserJwtRequest, res, next) => {
const isUserAuthor = (post: Post) => {
return (
req.user?.id &&
post.users?.map((user) => user.id).includes(req.user?.id)
)
}
try {
const post = await Post.findByPk(req.params.id, fullPostSequelizeOptions)
if (!post) { if (!post) {
return res.status(404).json({ error: "Post not found" }) return res.status(404).json({ error: "Post not found" })
} }
@ -301,9 +336,7 @@ posts.get(
} }
if (post.visibility === "public" || post?.visibility === "unlisted") { if (post.visibility === "public" || post?.visibility === "unlisted") {
secretKey(req, res, () => {
res.json(post) res.json(post)
})
} else if (post.visibility === "private") { } else if (post.visibility === "private") {
jwt(req as UserJwtRequest, res, () => { jwt(req as UserJwtRequest, res, () => {
if (isUserAuthor(post)) { if (isUserAuthor(post)) {
@ -313,27 +346,8 @@ posts.get(
} }
}) })
} else if (post.visibility === "protected") { } else if (post.visibility === "protected") {
const { password } = req.query // The client ensures to not send the post to the client.
if (!password || typeof password !== "string") { // See client/pages/post/[id].tsx::getServerSideProps
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." })
}
res.json(post) res.json(post)
} }
} catch (e) { } catch (e) {