@@ -122,7 +71,7 @@ const Auth = ({
auto
width="100%"
icon={
}
- onClick={() => signIn("github")}
+ onClick={() => signIn("github").catch((err) => setErrorMsg(err.message))}
>
Sign in with GitHub
diff --git a/client/components/badges/visibility-control/index.tsx b/client/components/badges/visibility-control/index.tsx
index 5a6bd665..1dfade5e 100644
--- a/client/components/badges/visibility-control/index.tsx
+++ b/client/components/badges/visibility-control/index.tsx
@@ -7,8 +7,8 @@ import { useCallback, useState } from "react"
type Props = {
postId: string
- visibility: PostVisibility
- setVisibility: (visibility: PostVisibility) => void
+ visibility: string
+ setVisibility: (visibility: string) => void
}
const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
@@ -17,12 +17,11 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
const { setToast } = useToasts()
const sendRequest = useCallback(
- async (visibility: PostVisibility, password?: string) => {
+ async (visibility: string, password?: string) => {
const res = await fetch(`/server-api/posts/${postId}`, {
method: "PUT",
headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${getCookie(TOKEN_COOKIE_NAME)}`
+ "Content-Type": "application/json"
},
body: JSON.stringify({ visibility, password })
})
@@ -33,7 +32,7 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
} else {
const json = await res.json()
setToast({
- text: json.error.message,
+ text: "An error occurred",
type: "error"
})
setPasswordModalVisible(false)
@@ -63,10 +62,7 @@ const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
setSubmitting(false)
}
- const submitPassword = useCallback(
- (password: string) => onSubmit("protected", password),
- [onSubmit]
- )
+ const submitPassword = (password: string) => onSubmit("protected", password)
return (
<>
diff --git a/client/components/link/index.tsx b/client/components/link/index.tsx
index c601a95d..40a66d41 100644
--- a/client/components/link/index.tsx
+++ b/client/components/link/index.tsx
@@ -1,4 +1,3 @@
-import { useRouter } from "next/router"
import NextLink from "next/link"
import styles from "./link.module.css"
diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx
index 85cd7bc0..88fef11a 100644
--- a/client/components/new-post/index.tsx
+++ b/client/components/new-post/index.tsx
@@ -15,8 +15,6 @@ import DatePicker from "react-datepicker"
import getTitleForPostCopy from "@lib/get-title-for-post-copy"
import Description from "./description"
import { PostWithFiles } from "@lib/server/prisma"
-import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
-import { getCookie } from "cookies-next"
const emptyDoc = {
title: "",
@@ -60,15 +58,13 @@ const Post = ({
title?: string
files?: DocumentType[]
password?: string
- userId: string
parentId?: string
}
) => {
const res = await fetch(url, {
method: "POST",
headers: {
- "Content-Type": "application/json",
- Authorization: `Bearer ${getCookie(TOKEN_COOKIE_NAME)}`
+ "Content-Type": "application/json"
},
body: JSON.stringify({
title,
@@ -83,8 +79,9 @@ const Post = ({
router.push(`/post/${json.id}`)
} else {
const json = await res.json()
+ console.error(json)
setToast({
- text: json.error.message || "Please fill out all fields",
+ text: "Please fill out all fields",
type: "error"
})
setPasswordModalVisible(false)
@@ -140,13 +137,11 @@ const Post = ({
return
}
- const cookieName = getCookie(USER_COOKIE_NAME)
- await sendRequest("/api/posts/create", {
+ await sendRequest("/api/post", {
title,
files: docs,
visibility,
password,
- userId: cookieName ? String(getCookie(USER_COOKIE_NAME)) : "",
expiresAt: expiresAt || null,
parentId: newPostParent
})
@@ -260,6 +255,7 @@ const Post = ({
)
return (
+ // 150 so the post dropdown doesn't overflow
diff --git a/client/components/page-seo/index.tsx b/client/components/page-seo/index.tsx
index 91368c68..9dec0100 100644
--- a/client/components/page-seo/index.tsx
+++ b/client/components/page-seo/index.tsx
@@ -2,7 +2,7 @@ import React from "react"
type PageSeoProps = {
title?: string
- description?: string | null
+ description?: string
isLoading?: boolean
isPrivate?: boolean
}
diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx
index 0fa73966..9fa80784 100644
--- a/client/components/post-list/index.tsx
+++ b/client/components/post-list/index.tsx
@@ -121,6 +121,7 @@ const PostList = ({ morePosts, initialPosts }: Props) => {
clearable
placeholder="Search..."
onChange={handleSearchChange}
+ disabled={Boolean(!posts?.length)}
/>
{!posts &&
Failed to load. }
diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx
index 28574605..012638e3 100644
--- a/client/components/post-list/list-item.tsx
+++ b/client/components/post-list/list-item.tsx
@@ -1,4 +1,3 @@
-import NextLink from "next/link"
import VisibilityBadge from "../badges/visibility-badge"
import {
Text,
@@ -13,12 +12,13 @@ import Trash from "@geist-ui/icons/trash"
import ExpirationBadge from "@components/badges/expiration-badge"
import CreatedAgoBadge from "@components/badges/created-ago-badge"
import Edit from "@geist-ui/icons/edit"
-import { useRouter } from "next/router"
+import { useRouter } from "next/navigation"
import Parent from "@geist-ui/icons/arrowUpCircle"
import styles from "./list-item.module.css"
import Link from "@components/link"
-import { PostWithFiles, File } from "@lib/server/prisma"
-import { PostVisibility } from "@lib/types"
+import type { PostWithFiles } from "@lib/server/prisma"
+import type { PostVisibility } from "@lib/types"
+import type { File } from "@lib/server/prisma"
// TODO: isOwner should default to false so this can be used generically
const ListItem = ({
@@ -40,6 +40,10 @@ const ListItem = ({
router.push(`/post/${post.parentId}`)
}
+ {
+ console.log(post)
+ }
+
return (
@@ -49,8 +53,7 @@ const ListItem = ({
{post.title}
@@ -94,7 +97,7 @@ const ListItem = ({
- {post.files?.map((file: File) => {
+ {post?.files?.map((file: File) => {
return (
@@ -105,7 +108,7 @@ const ListItem = ({
})}
- {" "}
+
)
}
diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx
index 733797de..40b6e8fc 100644
--- a/client/components/post-page/index.tsx
+++ b/client/components/post-page/index.tsx
@@ -1,56 +1,48 @@
-import PageSeo from "@components/page-seo"
+"use client"
+
import VisibilityBadge from "@components/badges/visibility-badge"
import DocumentComponent from "@components/view-document"
import styles from "./post-page.module.css"
-import homeStyles from "@styles/Home.module.css"
-import type { File, Post, PostVisibility } from "@lib/types"
-import {
- Page,
- Button,
- Text,
- ButtonGroup,
- useMediaQuery
-} from "@geist-ui/core/dist"
+import type { PostVisibility } from "@lib/types"
+import { Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist"
import { useEffect, useState } from "react"
import Archive from "@geist-ui/icons/archive"
import Edit from "@geist-ui/icons/edit"
import Parent from "@geist-ui/icons/arrowUpCircle"
import FileDropdown from "@components/file-dropdown"
import ScrollToTop from "@components/scroll-to-top"
-import { useRouter } from "next/router"
+import { useRouter } from "next/navigation"
import ExpirationBadge from "@components/badges/expiration-badge"
import CreatedAgoBadge from "@components/badges/created-ago-badge"
import PasswordModalPage from "./password-modal-wrapper"
import VisibilityControl from "@components/badges/visibility-control"
-import { USER_COOKIE_NAME } from "@lib/constants"
-import { getCookie } from "cookies-next"
+import { File, PostWithFiles } from "@lib/server/prisma"
+import Header from "@components/header"
type Props = {
- post: Post
+ post: PostWithFiles
isProtected?: boolean
+ isAuthor?: boolean
}
-const PostPage = ({ post: initialPost, isProtected }: Props) => {
- const [post, setPost] = useState
(initialPost)
- const [visibility, setVisibility] = useState(post.visibility)
+const PostPage = ({ post: initialPost, isProtected, isAuthor }: 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 === getCookie(USER_COOKIE_NAME) : false
- )
const router = useRouter()
const isMobile = useMediaQuery("mobile")
useEffect(() => {
- if (!isOwner && isExpired) {
+ if (!isAuthor && isExpired) {
router.push("/expired")
}
const expirationDate = new Date(post.expiresAt ? post.expiresAt : "")
- if (!isOwner && expirationDate < new Date()) {
+ if (!isAuthor && expirationDate < new Date()) {
router.push("/expired")
} else {
setIsLoading(false)
@@ -66,7 +58,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
return () => {
if (interval) clearInterval(interval)
}
- }, [isExpired, isOwner, post.expiresAt, post.users, router])
+ }, [isExpired, isAuthor, post.expiresAt, router])
const download = async () => {
if (!post.files) return
@@ -92,7 +84,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
}
const viewParentClick = () => {
- router.push(`/post/${post.parent!.id}`)
+ router.push(`/post/${post.parentId}`)
}
if (isLoading) {
@@ -103,77 +95,74 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
return (
<>
-
{!isAvailable && }
-
-
-
-
+
+
+ }
+ onClick={editACopy}
+ style={{ textTransform: "none" }}
>
- }
- onClick={editACopy}
- style={{ textTransform: "none" }}
- >
- Edit a Copy
+ Edit a Copy
+
+ {post.parent && (
+ } onClick={viewParentClick}>
+ View Parent
- {post.parent && (
- } onClick={viewParentClick}>
- View Parent
-
- )}
- }
- style={{ textTransform: "none" }}
- >
- Download as ZIP Archive
-
-
-
-
-
- {post.title}
-
-
-
-
-
+ )}
+ }
+ style={{ textTransform: "none" }}
+ >
+ Download as ZIP Archive
+
+
+
+
+
+ {post.title}
+
+
+
+
+
+
+ {post.description && (
+
+ {post.description}
- {post.description && (
-
- {post.description}
-
- )}
- {/* {post.files.length > 1 && } */}
- {post.files?.map(({ id, content, title }: File) => (
- 1 && } */}
+ {post.files?.map(({ id, content, title }: File) => (
+
+ ))}
+ {isAuthor && (
+
+
- ))}
- {isOwner && (
-
-
-
- )}
-
-
+
+ )}
+
>
)
}
diff --git a/client/components/post-page/password-modal-wrapper.tsx b/client/components/post-page/password-modal-wrapper.tsx
index cf5bf142..405ebc05 100644
--- a/client/components/post-page/password-modal-wrapper.tsx
+++ b/client/components/post-page/password-modal-wrapper.tsx
@@ -1,21 +1,22 @@
import PasswordModal from "@components/new-post/password-modal"
-import { Page, useToasts } from "@geist-ui/core/dist"
-import { Post } from "@lib/types"
-import { useRouter } from "next/router"
+import { useToasts } from "@geist-ui/core/dist"
+import { Post } from "@lib/server/prisma"
+import { useRouter } from "next/navigation"
import { useState } from "react"
type Props = {
setPost: (post: Post) => void
+ postId: Post["id"]
}
-const PasswordModalPage = ({ setPost }: Props) => {
+const PasswordModalPage = ({ setPost, postId }: 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}`,
+ `/api/posts/authenticate?id=${postId}&password=${password}`,
{
method: "GET",
headers: {
diff --git a/client/components/settings/sections/profile.tsx b/client/components/settings/sections/profile.tsx
index 055c840d..dae4d1c7 100644
--- a/client/components/settings/sections/profile.tsx
+++ b/client/components/settings/sections/profile.tsx
@@ -7,17 +7,10 @@ import { User } from "next-auth"
import { useEffect, useState } from "react"
const Profile = ({ user }: { user: User }) => {
- const [name, setName] = useState()
- const [email, setEmail] = useState()
+ const [name, setName] = useState(user.name || "")
+ const [email, setEmail] = useState(user.email || "")
const [bio, setBio] = useState()
- useEffect(() => {
- console.log(user)
- // if (user?.displayName) setName(user.displayName)
- if (user?.email) setEmail(user.email)
- // if (user?.bio) setBio(user.bio)
- }, [user])
-
const { setToast } = useToasts()
const handleNameChange = (e: React.ChangeEvent) => {
diff --git a/client/lib/api-middleware/with-methods.ts b/client/lib/api-middleware/with-methods.ts
new file mode 100644
index 00000000..3df37312
--- /dev/null
+++ b/client/lib/api-middleware/with-methods.ts
@@ -0,0 +1,13 @@
+// https://github.com/shadcn/taxonomy/
+
+import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next"
+
+export function withMethods(methods: string[], handler: NextApiHandler) {
+ return async function (req: NextApiRequest, res: NextApiResponse) {
+ if (!req.method || !methods.includes(req.method)) {
+ return res.status(405).end()
+ }
+
+ return handler(req, res)
+ }
+}
diff --git a/client/lib/api-middleware/with-validation.ts b/client/lib/api-middleware/with-validation.ts
new file mode 100644
index 00000000..f5da8203
--- /dev/null
+++ b/client/lib/api-middleware/with-validation.ts
@@ -0,0 +1,41 @@
+import type { NextApiHandler, NextApiRequest, NextApiResponse } from "next"
+import * as z from "zod"
+import type { ZodSchema, ZodType } from "zod"
+
+type NextApiRequestWithParsedBody = NextApiRequest & {
+ parsedBody?: T
+}
+
+export type NextApiHandlerWithParsedBody = (
+ req: NextApiRequestWithParsedBody,
+ res: NextApiResponse
+) => ReturnType
+
+export function withValidation(
+ schema: T,
+ handler: NextApiHandler
+): (
+ req: NextApiRequest,
+ res: NextApiResponse
+) => Promise | NextApiHandlerWithParsedBody> {
+ return async function (req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const body = req.body
+
+ await schema.parseAsync(body)
+
+ ;(req as NextApiRequestWithParsedBody).parsedBody = body
+
+ return handler(req, res) as Promise>
+ } catch (error) {
+ if (process.env.NODE_ENV === "development") {
+ console.error(error)
+ }
+ if (error instanceof z.ZodError) {
+ return res.status(422).json(error.issues)
+ }
+
+ return res.status(422).end()
+ }
+ }
+}
diff --git a/client/lib/server/auth.ts b/client/lib/server/auth.ts
index 9a702faa..add5d193 100644
--- a/client/lib/server/auth.ts
+++ b/client/lib/server/auth.ts
@@ -44,6 +44,7 @@ export const authOptions: NextAuthOptions = {
// TODO: user should be defined?
if (user) {
token.id = user.id
+ token.role = "user"
}
return token
}
diff --git a/client/lib/server/get-html-from-drift-file.ts b/client/lib/server/get-html-from-drift-file.ts
index bee7c351..5cee8216 100644
--- a/client/lib/server/get-html-from-drift-file.ts
+++ b/client/lib/server/get-html-from-drift-file.ts
@@ -28,9 +28,13 @@ export function getHtmlFromFile({
let contentToRender: string = content || ""
if (!renderAsMarkdown.includes(type)) {
- contentToRender = `~~~${type}
+ contentToRender = `
+
+~~~${type}
${content}
-~~~`
+~~~
+
+`
} else {
contentToRender = "\n" + content
}
diff --git a/client/lib/server/jwt.ts b/client/lib/server/jwt.ts
index f6343eb4..418d9c92 100644
--- a/client/lib/server/jwt.ts
+++ b/client/lib/server/jwt.ts
@@ -47,11 +47,11 @@ export async function withJwt(
select: {
id: true,
email: true,
- displayName: true,
- bio: true,
- createdAt: true,
- updatedAt: true,
- deletedAt: true
+ // displayName: true,
+ // bio: true,
+ // createdAt: true,
+ // updatedAt: true,
+ // deletedAt: true
}
})
if (!userObj) {
diff --git a/client/lib/server/prisma.ts b/client/lib/server/prisma.ts
index 6933238b..1b887c65 100644
--- a/client/lib/server/prisma.ts
+++ b/client/lib/server/prisma.ts
@@ -4,10 +4,45 @@ declare global {
import config from "@lib/config"
import { Post, PrismaClient, File, User } from "@prisma/client"
+import { cache } from "react"
import { generateAndExpireAccessToken } from "./generate-access-token"
const prisma = new PrismaClient()
+// we want to update iff they exist the createdAt/updated/expired/deleted items
+// the input could be an array, in which case we'd check each item in the array
+// if it's an object, we'd check that object
+// then we return the changed object or array
+
+const updateDateForItem = (item: any) => {
+ if (item.createdAt) {
+ item.createdAt = item.createdAt.toISOString()
+ }
+ if (item.updatedAt) {
+ item.updatedAt = item.updatedAt.toISOString()
+ }
+ if (item.expiresAt) {
+ item.expiresAt = item.expiresAt.toISOString()
+ }
+ if (item.deletedAt) {
+ item.deletedAt = item.deletedAt.toISOString()
+ }
+ return item
+}
+
+const updateDates = (input: any) => {
+ if (Array.isArray(input)) {
+ return input.map((item) => updateDateForItem(item))
+ } else {
+ return updateDateForItem(input)
+ }
+}
+
+prisma.$use(async (params, next) => {
+ const result = await next(params)
+ return updateDates(result)
+})
+
export default prisma
// https://next-auth.js.org/adapters/prisma
@@ -30,42 +65,14 @@ export const getFilesForPost = async (postId: string) => {
return files
}
-/**
- * When passed in a postId, fetches the post and then the files.
- * If passed a Post, it will fetch the files
- * @param postIdOrPost Post or postId
- * @returns Promise
- */
-export async function getPostWithFiles(postId: string): Promise
-export async function getPostWithFiles(postObject: Post): Promise
-export async function getPostWithFiles(
- postIdOrObject: string | Post
-): Promise {
- let post: Post | null
- if (typeof postIdOrObject === "string") {
- post = await prisma.post.findUnique({
- where: {
- id: postIdOrObject
- }
- })
- } else {
- post = postIdOrObject
- }
+export async function getFilesByPost(postId: string) {
+ const files = await prisma.file.findMany({
+ where: {
+ postId
+ }
+ })
- if (!post) {
- return undefined
- }
-
- const files = await getFilesForPost(post.id)
-
- if (!files) {
- return undefined
- }
-
- return {
- ...post,
- files
- }
+ return files
}
export async function getPostsByUser(userId: string): Promise
@@ -77,23 +84,12 @@ export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
const posts = await prisma.post.findMany({
where: {
authorId: userId
+ },
+ include: {
+ files: withFiles
}
})
- if (withFiles) {
- const postsWithFiles = await Promise.all(
- posts.map(async (post) => {
- const files = await getPostWithFiles(post)
- return {
- ...post,
- files
- }
- })
- )
-
- return postsWithFiles
- }
-
return posts
}
@@ -127,7 +123,11 @@ export const isUserAdmin = async (userId: User["id"]) => {
return user?.role?.toLowerCase() === "admin"
}
-export const createUser = async (username: string, password: string, serverPassword?: string) => {
+export const createUser = async (
+ username: string,
+ password: string,
+ serverPassword?: string
+) => {
if (!username || !password) {
throw new Error("Missing param")
}
@@ -141,9 +141,10 @@ export const createUser = async (username: string, password: string, serverPassw
}
// const salt = await genSalt(10)
-
+
// the first user is the admin
- const isUserAdminByDefault = config.enable_admin && (await prisma.user.count()) === 0
+ const isUserAdminByDefault =
+ config.enable_admin && (await prisma.user.count()) === 0
const userRole = isUserAdminByDefault ? "admin" : "user"
// const user = await prisma.user.create({
@@ -162,10 +163,14 @@ export const createUser = async (username: string, password: string, serverPassw
}
}
-export const getPostById = async (postId: Post["id"]) => {
+export const getPostById = async (postId: Post["id"], withFiles = false) => {
+ console.log("getPostById", postId)
const post = await prisma.post.findUnique({
where: {
id: postId
+ },
+ include: {
+ files: withFiles
}
})
diff --git a/client/lib/server/session.ts b/client/lib/server/session.ts
index dde592c3..7ea8a776 100644
--- a/client/lib/server/session.ts
+++ b/client/lib/server/session.ts
@@ -1,5 +1,4 @@
-import 'server-only';
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./auth"
diff --git a/client/lib/time-ago.ts b/client/lib/time-ago.ts
index 66e452c7..0688d9c4 100644
--- a/client/lib/time-ago.ts
+++ b/client/lib/time-ago.ts
@@ -10,7 +10,6 @@ const epochs = [
["second", 1]
] as const
-// Get duration
const getDuration = (timeAgoInSeconds: number) => {
for (let [name, seconds] of epochs) {
const interval = Math.floor(timeAgoInSeconds / seconds)
diff --git a/client/lib/validations/post.ts b/client/lib/validations/post.ts
new file mode 100644
index 00000000..5c870447
--- /dev/null
+++ b/client/lib/validations/post.ts
@@ -0,0 +1,18 @@
+import { z } from "zod"
+
+export const CreatePostSchema = z.object({
+ title: z.string(),
+ description: z.string(),
+ files: z.array(z.object({
+ title: z.string(),
+ content: z.string(),
+ })),
+ visibility: z.string(),
+ password: z.string().optional(),
+ expiresAt: z.number().optional().nullish(),
+ parentId: z.string().optional()
+})
+
+export const DeletePostSchema = z.object({
+ id: z.string()
+})
diff --git a/client/package.json b/client/package.json
index c6992f16..4e2443cf 100644
--- a/client/package.json
+++ b/client/package.json
@@ -25,6 +25,7 @@
"marked": "^4.2.2",
"next": "13.0.3-canary.4",
"next-auth": "^4.16.4",
+ "next-joi": "^2.2.1",
"next-themes": "npm:@wits/next-themes@0.2.7",
"prism-react-renderer": "^1.3.5",
"rc-table": "7.24.1",
diff --git a/client/pages/api/file/get-html.ts b/client/pages/api/file/get-html.ts
index 451140f6..cfadb1a6 100644
--- a/client/pages/api/file/get-html.ts
+++ b/client/pages/api/file/get-html.ts
@@ -1,52 +1,47 @@
+import { withMethods } from "@lib/api-middleware/with-methods"
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
import { parseQueryParam } from "@lib/server/parse-query-param"
import prisma from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next"
-export default async function handler(
+export default withMethods(["GET"], (
req: NextApiRequest,
res: NextApiResponse
-) {
- switch (req.method) {
- case "GET":
- const query = req.query
- const fileId = parseQueryParam(query.fileId)
- const content = parseQueryParam(query.content)
- const title = parseQueryParam(query.title)
+) => {
+ const query = req.query
+ const fileId = parseQueryParam(query.fileId)
+ const content = parseQueryParam(query.content)
+ const title = parseQueryParam(query.title)
- if (fileId && (content || title)) {
- return res.status(400).json({ error: "Too many arguments" })
+ if (fileId && (content || title)) {
+ return res.status(400).json({ error: "Too many arguments" })
+ }
+
+ if (fileId) {
+ const file = await prisma.file.findUnique({
+ where: {
+ id: fileId
}
+ })
- if (fileId) {
- // TODO: abstract to getFileById
- const file = await prisma.file.findUnique({
- where: {
- id: fileId
- }
- })
+ if (!file) {
+ return res.status(404).json({ error: "File not found" })
+ }
- if (!file) {
- return res.status(404).json({ error: "File not found" })
- }
+ return res.json(file.html)
+ } else {
+ if (!content || !title) {
+ return res.status(400).json({ error: "Missing arguments" })
+ }
- return res.json(file.html)
- } else {
- if (!content || !title) {
- return res.status(400).json({ error: "Missing arguments" })
- }
+ const renderedHTML = getHtmlFromFile({
+ title,
+ content
+ })
- const renderedHTML = getHtmlFromFile({
- title,
- content
- })
-
- res.setHeader("Content-Type", "text/plain")
- res.status(200).write(renderedHTML)
- res.end()
- return
- }
- default:
- return res.status(405).json({ error: "Method not allowed" })
+ res.setHeader("Content-Type", "text/plain")
+ res.status(200).write(renderedHTML)
+ res.end()
+ return
}
}
diff --git a/client/pages/api/file/html/[id].ts b/client/pages/api/file/html/[id].ts
index 6e07d360..dcc1aa28 100644
--- a/client/pages/api/file/html/[id].ts
+++ b/client/pages/api/file/html/[id].ts
@@ -1,24 +1,22 @@
import { NextApiRequest, NextApiResponse } from "next"
+import prisma from "lib/server/prisma"
+import { parseQueryParam } from "@lib/server/parse-query-param"
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
- const { id } = req.query
- const file = await fetch(`${process.env.API_URL}/files/html/${id}`, {
- headers: {
- "x-secret-key": process.env.SECRET_KEY || "",
- Authorization: `Bearer ${req.cookies["drift-token"]}`
+ const file = await prisma.file.findUnique({
+ where: {
+ id: parseQueryParam(req.query.id)
}
})
- if (file.ok) {
- const json = await file.text()
- const data = json
- // serve the file raw as plain text
- res.setHeader("Content-Type", "text/plain; charset=utf-8")
- res.setHeader("Cache-Control", "s-maxage=86400")
- res.status(200).write(data, "utf-8")
- res.end()
- } else {
- res.status(404).send("File not found")
+
+ if (!file) {
+ return res.status(404).end()
}
+
+ res.setHeader("Content-Type", "text/plain")
+ res.setHeader("Cache-Control", "public, max-age=4800")
+ console.log(file.html)
+ return res.status(200).write(file.html)
}
export default getRawFile
diff --git a/client/pages/api/post/index.ts b/client/pages/api/post/index.ts
new file mode 100644
index 00000000..b63d1145
--- /dev/null
+++ b/client/pages/api/post/index.ts
@@ -0,0 +1,136 @@
+// nextjs typescript api handler
+
+import { withCurrentUser } from "@lib/api-middleware/with-current-user"
+import { withMethods } from "@lib/api-middleware/with-methods"
+import {
+ NextApiHandlerWithParsedBody,
+ withValidation
+} from "@lib/api-middleware/with-validation"
+import { authOptions } from "@lib/server/auth"
+import { CreatePostSchema } from "@lib/validations/post"
+import { Post } from "@prisma/client"
+import prisma, { getPostById } from "lib/server/prisma"
+import { NextApiRequest, NextApiResponse } from "next"
+import { unstable_getServerSession } from "next-auth/next"
+import { File } from "lib/server/prisma"
+import * as crypto from "crypto"
+import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
+import { getSession } from "next-auth/react"
+import { parseQueryParam } from "@lib/server/parse-query-param"
+
+const handler = async (req: NextApiRequest, res: NextApiResponse) => {
+ if (req.method === "POST") {
+ return await handlePost(req, res)
+ } else {
+ return await handleGet(req, res)
+ }
+}
+
+export default withMethods(["POST", "GET"], handler)
+
+async function handlePost(req: NextApiRequest, res: NextApiResponse) {
+ try {
+ const session = await unstable_getServerSession(req, res, authOptions)
+
+ const files = req.body.files as File[]
+ const fileTitles = files.map((file) => file.title)
+ const missingTitles = fileTitles.filter((title) => title === "")
+ if (missingTitles.length > 0) {
+ throw new Error("All files must have a title")
+ }
+
+ if (files.length === 0) {
+ throw new Error("You must submit at least one file")
+ }
+
+ let hashedPassword: string = ""
+ if (req.body.visibility === "protected") {
+ hashedPassword = crypto
+ .createHash("sha256")
+ .update(req.body.password)
+ .digest("hex")
+ }
+
+ const postFiles = files.map((file) => {
+ const html = getHtmlFromFile(file)
+
+ return {
+ title: file.title,
+ content: file.content,
+ sha: crypto
+ .createHash("sha256")
+ .update(file.content)
+ .digest("hex")
+ .toString(),
+ html: html,
+ userId: session?.user.id
+ // postId: post.id
+ }
+ }) as File[]
+
+ const post = await prisma.post.create({
+ data: {
+ title: req.body.title,
+ description: req.body.description,
+ visibility: req.body.visibility,
+ password: hashedPassword,
+ expiresAt: req.body.expiresAt,
+ // authorId: session?.user.id,
+ author: {
+ connect: {
+ id: session?.user.id
+ }
+ },
+ files: {
+ create: postFiles
+ }
+ }
+ })
+
+ return res.json(post)
+ } catch (error) {
+ return res.status(500).json(error)
+ }
+}
+
+async function handleGet(req: NextApiRequest, res: NextApiResponse) {
+ const id = parseQueryParam(req.query.id)
+ const files = req.query.files ? parseQueryParam(req.query.files) : true
+
+ if (!id) {
+ return res.status(400).json({ error: "Missing id" })
+ }
+
+ const post = await getPostById(id, Boolean(files))
+
+ if (!post) {
+ return res.status(404).json({ message: "Post not found" })
+ }
+
+ if (post.visibility === "public") {
+ res.setHeader("Cache-Control", "s-maxage=86400, stale-while-revalidate")
+ return res.json(post)
+ } else if (post.visibility === "unlisted") {
+ res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate")
+ }
+
+ const session = await getSession({ req })
+
+ // the user can always go directly to their own post
+ if (session?.user.id === post.authorId) {
+ return res.json(post)
+ }
+
+ if (post.visibility === "protected") {
+ return {
+ isProtected: true,
+ post: {
+ id: post.id,
+ visibility: post.visibility,
+ title: post.title
+ }
+ }
+ }
+
+ return res.status(404).json({ message: "Post not found" })
+}
diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml
index 1126ac91..6997f362 100644
--- a/client/pnpm-lock.yaml
+++ b/client/pnpm-lock.yaml
@@ -26,6 +26,7 @@ specifiers:
marked: ^4.2.2
next: 13.0.3-canary.4
next-auth: ^4.16.4
+ next-joi: ^2.2.1
next-themes: npm:@wits/next-themes@0.2.7
next-unused: 0.0.6
prettier: 2.6.2
@@ -61,6 +62,7 @@ dependencies:
marked: 4.2.2
next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
next-auth: 4.16.4_hsmqkug4agizydugca45idewda
+ next-joi: 2.2.1_next@13.0.3-canary.4
next-themes: /@wits/next-themes/0.2.7_hsmqkug4agizydugca45idewda
prism-react-renderer: 1.3.5_react@18.2.0
rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y
@@ -2732,6 +2734,15 @@ packages:
uuid: 8.3.2
dev: false
+ /next-joi/2.2.1_next@13.0.3-canary.4:
+ resolution: {integrity: sha512-m6/rDj9a9sp0CeMGy3np/7T2663QFinfiTY4MuJ9LEicU+6SiDim4wnsqG5CfzI4IQX4tupN6jSCtsv0t2EWnQ==}
+ peerDependencies:
+ joi: '>=17.1.1'
+ next: '>=9.5.1'
+ dependencies:
+ next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
+ dev: false
+
/next-unused/0.0.6:
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
hasBin: true
diff --git a/client/prisma/schema.prisma b/client/prisma/schema.prisma
index c008285c..f5f64c76 100644
--- a/client/prisma/schema.prisma
+++ b/client/prisma/schema.prisma
@@ -57,7 +57,7 @@ model Post {
parentId String?
description String?
author User? @relation(fields: [authorId], references: [id])
- authorId String?
+ authorId String
files File[]
@@map("posts")
diff --git a/client/styles/globals.css b/client/styles/globals.css
index a914c885..2fa1eb06 100644
--- a/client/styles/globals.css
+++ b/client/styles/globals.css
@@ -161,3 +161,9 @@ code {
#__next {
isolation: isolate;
}
+
+/* TODO: this should not be necessary. */
+main {
+ margin-top: 0 !important;
+ padding-top: 0 !important;
+}