client: remove need for multiple post page URLs
This commit is contained in:
parent
4bcf791c86
commit
67e1b9889b
7 changed files with 189 additions and 139 deletions
|
@ -37,7 +37,7 @@ const PasswordModal = ({
|
|||
{/* TODO: investigate disableBackdropClick not updating state? */}
|
||||
|
||||
{
|
||||
<Modal visible={isOpen} disableBackdropClick={true}>
|
||||
<Modal visible={isOpen} disableBackdropClick={false}>
|
||||
<Modal.Title>Enter a password</Modal.Title>
|
||||
<Modal.Content>
|
||||
{!error && creating && (
|
||||
|
|
|
@ -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<Post>(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 (
|
||||
<Page width={"100%"}>
|
||||
<PageSeo
|
||||
|
@ -91,7 +97,7 @@ const PostPage = ({ post }: Props) => {
|
|||
description={post.description}
|
||||
isPrivate={false}
|
||||
/>
|
||||
|
||||
{!isAvailable && <PasswordModalPage setPost={setPost} />}
|
||||
<Page.Content className={homeStyles.main}>
|
||||
<div className={styles.header}>
|
||||
<span className={styles.buttons}>
|
||||
|
|
64
client/components/post-page/password-modal-wrapper.tsx
Normal file
64
client/components/post-page/password-modal-wrapper.tsx
Normal 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
|
|
@ -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}`
|
||||
|
|
|
@ -5,31 +5,37 @@ import PostPage from "@components/post-page"
|
|||
|
||||
export type PostProps = {
|
||||
post: Post
|
||||
isProtected?: boolean
|
||||
}
|
||||
|
||||
const PostView = ({ post }: PostProps) => {
|
||||
return <PostPage post={post} />
|
||||
const PostView = ({ post, isProtected }: PostProps) => {
|
||||
return <PostPage isProtected={isProtected} post={post} />
|
||||
}
|
||||
|
||||
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: {
|
||||
|
|
|
@ -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
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue