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? */}
|
{/* 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 && (
|
||||||
|
|
|
@ -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}>
|
||||||
|
|
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) {
|
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}`
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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(
|
posts.get(
|
||||||
"/:id",
|
"/:id",
|
||||||
|
secretKey,
|
||||||
celebrate({
|
celebrate({
|
||||||
params: {
|
params: {
|
||||||
id: Joi.string().required()
|
id: Joi.string().required()
|
||||||
|
@ -254,42 +324,7 @@ posts.get(
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const post = await Post.findByPk(req.params.id, {
|
const post = await Post.findByPk(req.params.id, 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",
|
|
||||||
"password"
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
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) {
|
||||||
|
|
Loading…
Reference in a new issue