refactor to SWR and verifyApiUser; personal post search is broken
This commit is contained in:
parent
6fb81d77b9
commit
ba732dcd71
41 changed files with 379 additions and 328 deletions
|
@ -1,14 +1,9 @@
|
|||
{
|
||||
"extends": [
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"root": true,
|
||||
"ignorePatterns": ["node_modules/", "__tests__/"],
|
||||
"rules": {
|
||||
"no-mixed-spaces-and-tabs": ["off"]
|
||||
}
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
|
||||
"ignorePatterns": ["node_modules/", "__tests__/"],
|
||||
"rules": {
|
||||
"@typescript-eslint/no-unused-vars": "error",
|
||||
"@typescript-eslint/no-explicit-any": "error"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ const nextConfig = {
|
|||
},
|
||||
images: {
|
||||
domains: ["avatars.githubusercontent.com"]
|
||||
},
|
||||
env: {
|
||||
NEXT_PUBLIC_DRIFT_URL: process.env.VERCEL_URL || process.env.DRIFT_URL
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useToasts } from "@components/toasts"
|
|||
import { useRouter, useSearchParams } from "next/navigation"
|
||||
import Note from "@components/note"
|
||||
|
||||
const Auth = ({
|
||||
function Auth({
|
||||
page,
|
||||
requiresServerPassword,
|
||||
isGithubEnabled
|
||||
|
@ -19,7 +19,7 @@ const Auth = ({
|
|||
page: "signup" | "signin"
|
||||
requiresServerPassword?: boolean
|
||||
isGithubEnabled?: boolean
|
||||
}) => {
|
||||
}) {
|
||||
const [serverPassword, setServerPassword] = useState("")
|
||||
const { setToast } = useToasts()
|
||||
const signingIn = page === "signin"
|
||||
|
@ -38,7 +38,7 @@ const Auth = ({
|
|||
}
|
||||
}, [queryParams, setToast])
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
|
||||
const res = await signIn("credentials", {
|
||||
|
@ -62,17 +62,17 @@ const Auth = ({
|
|||
}
|
||||
}
|
||||
|
||||
const handleChangeUsername = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
function handleChangeUsername(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setUsername(event.target.value)
|
||||
}
|
||||
|
||||
const handleChangePassword = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
function handleChangePassword(event: React.ChangeEvent<HTMLInputElement>) {
|
||||
setPassword(event.target.value)
|
||||
}
|
||||
|
||||
const handleChangeServerPassword = (
|
||||
function handleChangeServerPassword(
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
) {
|
||||
setServerPassword(event.target.value)
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import Auth from "../components"
|
|||
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
|
||||
import config from "@lib/config"
|
||||
|
||||
const getPasscode = async () => {
|
||||
async function getPasscode() {
|
||||
return await getRequiresPasscode()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,13 +8,12 @@ import { ChevronDown, Code, File as FileIcon } from "react-feather"
|
|||
import { Spinner } from "@components/spinner"
|
||||
import Link from "next/link"
|
||||
|
||||
const FileDropdown = ({
|
||||
files,
|
||||
loading
|
||||
function FileDropdown({
|
||||
files, loading
|
||||
}: {
|
||||
files: Pick<PostWithFiles, "files">["files"]
|
||||
loading?: boolean
|
||||
}) => {
|
||||
}) {
|
||||
if (loading) {
|
||||
return (
|
||||
<Popover>
|
||||
|
|
|
@ -3,6 +3,7 @@ import styles from "./preview.module.css"
|
|||
import "@styles/markdown.css"
|
||||
import "@styles/syntax.css"
|
||||
import { Spinner } from "@components/spinner"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
||||
type Props = {
|
||||
height?: number | string
|
||||
|
@ -11,12 +12,9 @@ type Props = {
|
|||
title?: string
|
||||
}
|
||||
|
||||
const MarkdownPreview = ({
|
||||
height = 500,
|
||||
fileId,
|
||||
content = "",
|
||||
title
|
||||
}: Props) => {
|
||||
function MarkdownPreview({
|
||||
height = 500, fileId, content = "", title
|
||||
}: Props) {
|
||||
const [preview, setPreview] = useState<string>(content)
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true)
|
||||
useEffect(() => {
|
||||
|
@ -27,11 +25,11 @@ const MarkdownPreview = ({
|
|||
const body = fileId
|
||||
? undefined
|
||||
: JSON.stringify({
|
||||
title: title || "",
|
||||
content: content
|
||||
})
|
||||
title: title || "",
|
||||
content: content
|
||||
})
|
||||
|
||||
const resp = await fetch(path, {
|
||||
const resp = await fetchWithUser(path, {
|
||||
method: method,
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
@ -62,20 +60,18 @@ const MarkdownPreview = ({
|
|||
|
||||
export default memo(MarkdownPreview)
|
||||
|
||||
export const StaticPreview = ({
|
||||
preview,
|
||||
height = 500
|
||||
export function StaticPreview({
|
||||
preview, height = 500
|
||||
}: {
|
||||
preview: string
|
||||
height: string | number
|
||||
}) => {
|
||||
}) {
|
||||
return (
|
||||
<article
|
||||
className={styles.markdownPreview}
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
style={{
|
||||
height
|
||||
}}
|
||||
/>
|
||||
}} />
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import * as RadixTabs from "@radix-ui/react-tabs"
|
||||
import FormattingIcons from "src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons"
|
||||
import { ChangeEvent, useRef } from "react"
|
||||
import { ChangeEvent, ClipboardEvent, useRef } from "react"
|
||||
import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
|
||||
import Preview, { StaticPreview } from "../preview"
|
||||
import styles from "./tabs.module.css"
|
||||
|
@ -11,7 +11,7 @@ type Props = RadixTabs.TabsProps & {
|
|||
isEditing: boolean
|
||||
defaultTab: "preview" | "edit"
|
||||
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
onPaste?: (e: any) => void
|
||||
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
|
||||
title?: string
|
||||
content?: string
|
||||
preview?: string
|
||||
|
|
|
@ -8,7 +8,7 @@ type props = {
|
|||
description: string
|
||||
}
|
||||
|
||||
const Description = ({ onChange, description }: props) => {
|
||||
function Description({ onChange, description }: props) {
|
||||
return (
|
||||
<div className={styles.description}>
|
||||
<Input
|
||||
|
@ -17,8 +17,7 @@ const Description = ({ onChange, description }: props) => {
|
|||
label="Description"
|
||||
maxLength={256}
|
||||
width="100%"
|
||||
placeholder="An optional description of your post"
|
||||
/>
|
||||
placeholder="An optional description of your post" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -14,21 +14,19 @@ import Button from "@components/button"
|
|||
import clsx from "clsx"
|
||||
// TODO: clean up
|
||||
|
||||
const FormattingIcons = ({
|
||||
textareaRef,
|
||||
className
|
||||
function FormattingIcons({
|
||||
textareaRef, className
|
||||
}: {
|
||||
textareaRef?: RefObject<TextareaMarkdownRef>
|
||||
className?: string
|
||||
}) => {
|
||||
}) {
|
||||
const formattingActions = useMemo(() => {
|
||||
const handleBoldClick = () => textareaRef?.current?.trigger("bold")
|
||||
const handleItalicClick = () => textareaRef?.current?.trigger("italic")
|
||||
const handleLinkClick = () => textareaRef?.current?.trigger("link")
|
||||
const handleImageClick = () => textareaRef?.current?.trigger("image")
|
||||
const handleCodeClick = () => textareaRef?.current?.trigger("code")
|
||||
const handleListClick = () =>
|
||||
textareaRef?.current?.trigger("unordered-list")
|
||||
const handleListClick = () => textareaRef?.current?.trigger("unordered-list")
|
||||
return [
|
||||
{
|
||||
icon: <Bold />,
|
||||
|
@ -83,8 +81,7 @@ const FormattingIcons = ({
|
|||
iconRight={icon}
|
||||
onMouseDown={(e) => e.preventDefault()}
|
||||
onClick={action}
|
||||
buttonType="secondary"
|
||||
/>
|
||||
buttonType="secondary" />
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { ChangeEvent, useCallback } from "react"
|
||||
import { ChangeEvent, ClipboardEvent, useCallback } from "react"
|
||||
import styles from "./document.module.css"
|
||||
import Button from "@components/button"
|
||||
import Input from "@components/input"
|
||||
|
@ -12,10 +12,10 @@ type Props = {
|
|||
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
|
||||
defaultTab?: "edit" | "preview"
|
||||
remove?: () => void
|
||||
onPaste?: (e: any) => void
|
||||
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
|
||||
}
|
||||
|
||||
const Document = ({
|
||||
function Document({
|
||||
onPaste,
|
||||
remove,
|
||||
title,
|
||||
|
@ -23,7 +23,7 @@ const Document = ({
|
|||
setTitle,
|
||||
defaultTab = "edit",
|
||||
handleOnContentChange
|
||||
}: Props) => {
|
||||
}: Props) {
|
||||
const onTitleChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) =>
|
||||
setTitle ? setTitle(event.target.value) : null,
|
||||
|
@ -84,6 +84,8 @@ const Document = ({
|
|||
isEditing={true}
|
||||
defaultTab={defaultTab}
|
||||
handleOnContentChange={handleOnContentChange}
|
||||
// TODO: solve types
|
||||
// @ts-expect-error Type 'HTMLDivElement' is missing the following properties from type 'HTMLTextAreaElement': autocomplete, cols, defaultValue, dirName, and 26 more
|
||||
onPaste={onPaste}
|
||||
title={title}
|
||||
content={content}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import type { Document } from "../new"
|
||||
import DocumentComponent from "./edit-document"
|
||||
import { ChangeEvent, useCallback } from "react"
|
||||
import { ChangeEvent, useCallback, ClipboardEvent } from "react"
|
||||
|
||||
const DocumentList = ({
|
||||
function DocumentList({
|
||||
docs,
|
||||
removeDoc,
|
||||
updateDocContent,
|
||||
|
@ -13,8 +13,8 @@ const DocumentList = ({
|
|||
updateDocTitle: (i: number) => (title: string) => void
|
||||
updateDocContent: (i: number) => (content: string) => void
|
||||
removeDoc: (i: number) => () => void
|
||||
onPaste: (e: any) => void
|
||||
}) => {
|
||||
onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
|
||||
}) {
|
||||
const handleOnChange = useCallback(
|
||||
(i: number) => (e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateDocContent(i)(e.target.value)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useState } from "react"
|
||||
import { useCallback, useState, ClipboardEvent } from "react"
|
||||
import generateUUID from "@lib/generate-uuid"
|
||||
import styles from "./post.module.css"
|
||||
import EditDocumentList from "./edit-document-list"
|
||||
|
@ -17,7 +17,8 @@ import Button from "@components/button"
|
|||
import Input from "@components/input"
|
||||
import ButtonDropdown from "@components/button-dropdown"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
||||
const emptyDoc = {
|
||||
title: "",
|
||||
|
@ -31,14 +32,14 @@ export type Document = {
|
|||
id: string
|
||||
}
|
||||
|
||||
const Post = ({
|
||||
function Post({
|
||||
initialPost: stringifiedInitialPost,
|
||||
newPostParent
|
||||
}: {
|
||||
initialPost?: string
|
||||
newPostParent?: string
|
||||
}) => {
|
||||
const session = useSession()
|
||||
}): JSX.Element | null {
|
||||
const { isAuthenticated } = useSessionSWR()
|
||||
|
||||
const parsedPost = JSON.parse(stringifiedInitialPost || "{}") as PostWithFiles
|
||||
const initialPost = parsedPost?.id ? parsedPost : null
|
||||
|
@ -74,7 +75,7 @@ const Post = ({
|
|||
parentId?: string
|
||||
}
|
||||
) => {
|
||||
const res = await fetch(url, {
|
||||
const res = await fetchWithUser(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
@ -115,7 +116,6 @@ const Post = ({
|
|||
}
|
||||
|
||||
setPasswordModalVisible(false)
|
||||
|
||||
setSubmitting(true)
|
||||
|
||||
let hasErrored = false
|
||||
|
@ -164,15 +164,6 @@ const Post = ({
|
|||
[docs, expiresAt, newPostParent, sendRequest, setToast, title]
|
||||
)
|
||||
|
||||
const onClosePasswordModal = () => {
|
||||
setPasswordModalVisible(false)
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
const submitPassword = (password: string) => onSubmit("protected", password)
|
||||
|
||||
const onChangeExpiration = (date: Date) => setExpiresAt(date)
|
||||
|
||||
const onChangeTitle = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
setTitle(e.target.value)
|
||||
|
@ -186,28 +177,47 @@ const Post = ({
|
|||
[]
|
||||
)
|
||||
|
||||
if (session.status === "unauthenticated") {
|
||||
if (isAuthenticated === false) {
|
||||
router.push("/signin")
|
||||
return null
|
||||
}
|
||||
|
||||
const updateDocTitle = (i: number) => (title: string) => {
|
||||
setDocs((docs) =>
|
||||
docs.map((doc, index) => (i === index ? { ...doc, title } : doc))
|
||||
)
|
||||
function onClosePasswordModal() {
|
||||
setPasswordModalVisible(false)
|
||||
setSubmitting(false)
|
||||
}
|
||||
|
||||
const updateDocContent = (i: number) => (content: string) => {
|
||||
setDocs((docs) =>
|
||||
docs.map((doc, index) => (i === index ? { ...doc, content } : doc))
|
||||
)
|
||||
function submitPassword(password: string) {
|
||||
return onSubmit("protected", password)
|
||||
}
|
||||
|
||||
const removeDoc = (i: number) => () => {
|
||||
setDocs((docs) => docs.filter((_, index) => i !== index))
|
||||
function onChangeExpiration(date: Date) {
|
||||
return setExpiresAt(date)
|
||||
}
|
||||
|
||||
const uploadDocs = (files: Document[]) => {
|
||||
function updateDocTitle(i: number) {
|
||||
return (title: string) => {
|
||||
setDocs((docs) =>
|
||||
docs.map((doc, index) => (i === index ? { ...doc, title } : doc))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function updateDocContent(i: number) {
|
||||
return (content: string) => {
|
||||
setDocs((docs) =>
|
||||
docs.map((doc, index) => (i === index ? { ...doc, content } : doc))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function removeDoc(i: number) {
|
||||
return () => {
|
||||
setDocs((docs) => docs.filter((_, index) => i !== index))
|
||||
}
|
||||
}
|
||||
|
||||
function uploadDocs(files: Document[]) {
|
||||
// if no title is set and the only document is empty,
|
||||
const isFirstDocEmpty =
|
||||
docs.length <= 1 && (docs.length ? docs[0].title === "" : true)
|
||||
|
@ -224,7 +234,7 @@ const Post = ({
|
|||
else setDocs((docs) => [...docs, ...files])
|
||||
}
|
||||
|
||||
const onPaste = (e: ClipboardEvent) => {
|
||||
function onPaste(e: ClipboardEvent<HTMLTextAreaElement>) {
|
||||
const pastedText = e.clipboardData?.getData("text")
|
||||
|
||||
if (pastedText) {
|
||||
|
@ -234,32 +244,6 @@ const Post = ({
|
|||
}
|
||||
}
|
||||
|
||||
const CustomTimeInput = ({
|
||||
date,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
date: Date
|
||||
value: string
|
||||
onChange: (date: string) => void
|
||||
}) => (
|
||||
<input
|
||||
type="time"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (!isNaN(date.getTime())) {
|
||||
onChange(e.target.value || date.toISOString().slice(11, 16))
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "var(--bg)",
|
||||
border: "1px solid var(--light-gray)",
|
||||
borderRadius: "var(--radius)"
|
||||
}}
|
||||
required
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<Title title={title} onChange={onChangeTitle} />
|
||||
|
@ -345,3 +329,31 @@ const Post = ({
|
|||
}
|
||||
|
||||
export default Post
|
||||
|
||||
function CustomTimeInput({
|
||||
date,
|
||||
value,
|
||||
onChange
|
||||
}: {
|
||||
date: Date
|
||||
value: string
|
||||
onChange: (date: string) => void
|
||||
}) {
|
||||
return (
|
||||
<input
|
||||
type="time"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
if (!isNaN(date.getTime())) {
|
||||
onChange(e.target.value || date.toISOString().slice(11, 16))
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: "var(--bg)",
|
||||
border: "1px solid var(--light-gray)",
|
||||
borderRadius: "var(--radius)"
|
||||
}}
|
||||
required
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ type props = {
|
|||
title?: string
|
||||
}
|
||||
|
||||
const Title = ({ onChange, title }: props) => {
|
||||
function Title({ onChange, title }: props) {
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
<h1 style={{ margin: 0, padding: 0 }}>Drift</h1>
|
||||
|
@ -31,8 +31,7 @@ const Title = ({ onChange, title }: props) => {
|
|||
label="Title"
|
||||
className={styles.labelAndInput}
|
||||
style={{ width: "100%" }}
|
||||
labelClassName={styles.labelAndInput}
|
||||
/>
|
||||
labelClassName={styles.labelAndInput} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@ import { notFound, redirect } from "next/navigation"
|
|||
import { getPostById } from "@lib/server/prisma"
|
||||
import { getSession } from "@lib/server/session"
|
||||
|
||||
const NewFromExisting = async ({
|
||||
async function NewFromExisting({
|
||||
params
|
||||
}: {
|
||||
params: {
|
||||
id: string
|
||||
}
|
||||
}) => {
|
||||
}) {
|
||||
const session = await getSession()
|
||||
if (!session?.user) {
|
||||
return redirect("/signin")
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import NewPost from "src/app/(posts)/new/components/new"
|
||||
import "./react-datepicker.css"
|
||||
|
||||
const New = () => <NewPost />
|
||||
|
||||
export default New
|
||||
export default function New() {
|
||||
return <NewPost />
|
||||
}
|
||||
|
||||
export const dynamic = "force-static"
|
||||
|
|
|
@ -5,7 +5,8 @@ import PasswordModal from "@components/password-modal"
|
|||
import { useRouter } from "next/navigation"
|
||||
import { useCallback, useEffect, useState } from "react"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
||||
type Props = {
|
||||
setPost: (post: PostWithFilesAndAuthor) => void
|
||||
|
@ -16,20 +17,22 @@ type Props = {
|
|||
const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
|
||||
const router = useRouter()
|
||||
const { setToast } = useToasts()
|
||||
const { data: session, status } = useSession()
|
||||
const isAuthor =
|
||||
status === "loading"
|
||||
? undefined
|
||||
: session?.user && session?.user?.id === authorId
|
||||
const { session, isLoading } = useSessionSWR()
|
||||
const isAuthor = isLoading
|
||||
? undefined
|
||||
: session?.user && session?.user?.id === authorId
|
||||
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
|
||||
const onSubmit = useCallback(
|
||||
async (password: string) => {
|
||||
const res = await fetch(`/api/post/${postId}?password=${password}`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
const res = await fetchWithUser(
|
||||
`/api/post/${postId}?password=${password}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
setToast({
|
||||
|
|
|
@ -6,6 +6,7 @@ import { useToasts } from "@components/toasts"
|
|||
import { Post, User } from "@lib/server/prisma"
|
||||
import Link from "next/link"
|
||||
import { useState } from "react"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
import styles from "./table.module.css"
|
||||
|
||||
export function UserTable({
|
||||
|
@ -25,7 +26,7 @@ export function UserTable({
|
|||
|
||||
const deleteUser = async (id: string) => {
|
||||
try {
|
||||
const res = await fetch("/api/admin?action=delete-user", {
|
||||
const res = await fetchWithUser("/api/admin?action=delete-user", {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
|
|
@ -6,8 +6,9 @@ import ButtonGroup from "@components/button-group"
|
|||
import Button from "@components/button"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { Spinner } from "@components/spinner"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
||||
type Props = {
|
||||
authorId: string
|
||||
|
@ -20,7 +21,7 @@ const VisibilityControl = ({
|
|||
postId,
|
||||
visibility: postVisibility
|
||||
}: Props) => {
|
||||
const { data: session } = useSession()
|
||||
const { session } = useSessionSWR()
|
||||
const isAuthor = session?.user && session?.user?.id === authorId
|
||||
const [visibility, setVisibility] = useState<string>(postVisibility)
|
||||
|
||||
|
@ -31,13 +32,16 @@ const VisibilityControl = ({
|
|||
|
||||
const sendRequest = useCallback(
|
||||
async (visibility: string, password?: string) => {
|
||||
const res = await fetch(`/api/post/${postId}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ visibility, password })
|
||||
})
|
||||
const res = await fetchWithUser(
|
||||
`/api/post/${postId}`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({ visibility, password })
|
||||
}
|
||||
)
|
||||
|
||||
if (res.ok) {
|
||||
const json = await res.json()
|
||||
|
|
|
@ -4,7 +4,7 @@ import styles from "./header.module.css"
|
|||
// import useUserData from "@lib/hooks/use-user-data"
|
||||
import Link from "@components/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { signOut, useSession } from "next-auth/react"
|
||||
import { signOut } from "next-auth/react"
|
||||
import Button from "@components/button"
|
||||
import clsx from "clsx"
|
||||
import { useTheme } from "next-themes"
|
||||
|
@ -23,6 +23,7 @@ import {
|
|||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
|
||||
import buttonStyles from "@components/button/button.module.css"
|
||||
import { useMemo } from "react"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
|
||||
type Tab = {
|
||||
name: string
|
||||
|
@ -33,10 +34,7 @@ type Tab = {
|
|||
}
|
||||
|
||||
const Header = () => {
|
||||
const session = useSession()
|
||||
const isSignedIn = session?.status === "authenticated"
|
||||
const isAdmin = session?.data?.user?.role === "admin"
|
||||
const isLoading = session?.status === "loading"
|
||||
const { isAuthenticated, isAdmin, isLoading, mutate } = useSessionSWR()
|
||||
|
||||
const pathname = usePathname()
|
||||
const { setTheme, resolvedTheme } = useTheme()
|
||||
|
@ -97,7 +95,7 @@ const Header = () => {
|
|||
value: "theme"
|
||||
})
|
||||
|
||||
if (isSignedIn)
|
||||
if (isAuthenticated)
|
||||
return [
|
||||
{
|
||||
name: "New",
|
||||
|
@ -122,10 +120,12 @@ const Header = () => {
|
|||
name: "Sign Out",
|
||||
icon: <UserX />,
|
||||
value: "signout",
|
||||
onClick: () =>
|
||||
onClick: () => {
|
||||
mutate(undefined)
|
||||
signOut({
|
||||
callbackUrl: "/"
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
else
|
||||
|
@ -150,7 +150,7 @@ const Header = () => {
|
|||
href: "/signup"
|
||||
}
|
||||
]
|
||||
}, [isAdmin, resolvedTheme, isSignedIn, setTheme])
|
||||
}, [isAdmin, resolvedTheme, isAuthenticated, setTheme, mutate])
|
||||
|
||||
const buttons = pages.map(getButton)
|
||||
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useToasts } from "@components/toasts"
|
|||
import { ListItemSkeleton } from "./list-item-skeleton"
|
||||
import Link from "@components/link"
|
||||
import debounce from "lodash.debounce"
|
||||
import { fetchWithUser } from "src/app/lib/fetch-with-user"
|
||||
|
||||
type Props = {
|
||||
initialPosts: string | PostWithFiles[]
|
||||
|
@ -36,6 +37,7 @@ const PostList = ({
|
|||
const [searchValue, setSearchValue] = useState("")
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
|
||||
|
||||
const { setToast } = useToasts()
|
||||
|
||||
const showSkeleton = skeleton || searching
|
||||
|
@ -51,8 +53,8 @@ const PostList = ({
|
|||
|
||||
setSearching(true)
|
||||
async function fetchPosts() {
|
||||
const res = await fetch(
|
||||
`/api/post/search?q=${encodeURIComponent(query)}&userId=${userId}`,
|
||||
const res = await fetchWithUser(
|
||||
`/api/post/search?q=${encodeURIComponent(query)}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
|
@ -61,6 +63,7 @@ const PostList = ({
|
|||
}
|
||||
)
|
||||
const json = await res.json()
|
||||
console.log(json)
|
||||
setPosts(json)
|
||||
setSearching(false)
|
||||
}
|
||||
|
@ -79,22 +82,22 @@ const PostList = ({
|
|||
|
||||
const deletePost = useCallback(
|
||||
(postId: string) => async () => {
|
||||
const res = await fetch(`/api/post/${postId}`, {
|
||||
const res = await fetchWithUser(`/api/post/${postId}`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
if (!res?.ok) {
|
||||
console.error(res)
|
||||
return
|
||||
} else {
|
||||
setPosts((posts) => posts.filter((post) => post.id !== postId))
|
||||
setPosts((posts) => posts?.filter((post) => post.id !== postId))
|
||||
setToast({
|
||||
message: "Post deleted",
|
||||
type: "success"
|
||||
})
|
||||
}
|
||||
},
|
||||
[setToast]
|
||||
[setPosts, setToast]
|
||||
)
|
||||
|
||||
return (
|
||||
|
@ -118,7 +121,7 @@ const PostList = ({
|
|||
<ListItemSkeleton />
|
||||
</ul>
|
||||
)}
|
||||
{!showSkeleton && posts?.length > 0 && (
|
||||
{!showSkeleton && posts && posts.length > 0 ? (
|
||||
<div>
|
||||
<ul>
|
||||
{posts.map((post) => {
|
||||
|
@ -134,7 +137,7 @@ const PostList = ({
|
|||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ const ListItem = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<FadeIn as="li" key={post.id}>
|
||||
<FadeIn key={post.id}>
|
||||
<li>
|
||||
<Card style={{ overflowY: "scroll" }}>
|
||||
<>
|
||||
|
|
|
@ -16,17 +16,7 @@ const TOKENS_ENDPOINT = "/api/user/tokens"
|
|||
|
||||
export function useApiTokens({ userId, initialTokens }: UseApiTokens) {
|
||||
const { data, mutate, error, isLoading } = useSWR<SerializedApiToken[]>(
|
||||
"/api/user/tokens?userId=" + userId,
|
||||
async (url: string) => {
|
||||
return fetch(url).then(async (res) => {
|
||||
const data = await res.json()
|
||||
if (data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
},
|
||||
userId ? "/api/user/tokens?userId=" + userId : null,
|
||||
{
|
||||
refreshInterval: 10000,
|
||||
fallbackData: initialTokens
|
||||
|
|
|
@ -13,7 +13,8 @@ interface RootLayoutProps {
|
|||
|
||||
export default async function RootLayout({ children }: RootLayoutProps) {
|
||||
return (
|
||||
<html lang="en" className={inter.variable}>
|
||||
// suppressHydrationWarning is required because of next-themes
|
||||
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
||||
<head />
|
||||
<body>
|
||||
<Toasts />
|
||||
|
|
12
src/app/lib/fetch-with-user.ts
Normal file
12
src/app/lib/fetch-with-user.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { getSession } from "next-auth/react"
|
||||
|
||||
/**
|
||||
* a fetch wrapper that adds `userId={userId}` to the query string
|
||||
*/
|
||||
export async function fetchWithUser(url: string, options: RequestInit = {}) {
|
||||
// TODO: figure out if this extra network call hurts performance
|
||||
const session = await getSession()
|
||||
const newUrl = new URL(url, process.env.NEXT_PUBLIC_DRIFT_URL)
|
||||
newUrl.searchParams.append("userId", session?.user.id || "")
|
||||
return fetch(newUrl.toString(), options)
|
||||
}
|
|
@ -6,7 +6,7 @@ import { getAllPosts, Post } from "@lib/server/prisma"
|
|||
import PostList, { NoPostsFound } from "@components/post-list"
|
||||
import { Suspense } from "react"
|
||||
|
||||
const getWelcomeData = async () => {
|
||||
export async function getWelcomeData() {
|
||||
const welcomeContent = await getWelcomeContent()
|
||||
return welcomeContent
|
||||
}
|
||||
|
|
|
@ -3,15 +3,29 @@
|
|||
import * as RadixTooltip from "@radix-ui/react-tooltip"
|
||||
import { SessionProvider } from "next-auth/react"
|
||||
import { ThemeProvider } from "next-themes"
|
||||
import { SWRConfig } from "swr"
|
||||
|
||||
export function Providers({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<SessionProvider>
|
||||
<RadixTooltip.Provider delayDuration={200}>
|
||||
<ThemeProvider enableSystem defaultTheme="dark">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</RadixTooltip.Provider>
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: async (url: string) => {
|
||||
const data = await fetch(url).then((res) => res.json())
|
||||
if (data.error) {
|
||||
throw new Error(data.error)
|
||||
}
|
||||
return data
|
||||
},
|
||||
keepPreviousData: true
|
||||
}}
|
||||
>
|
||||
<RadixTooltip.Provider delayDuration={200}>
|
||||
<ThemeProvider enableSystem defaultTheme="dark">
|
||||
{children}
|
||||
</ThemeProvider>
|
||||
</RadixTooltip.Provider>
|
||||
</SWRConfig>
|
||||
</SessionProvider>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -10,9 +10,9 @@ import {
|
|||
useApiTokens
|
||||
} from "src/app/hooks/swr/use-api-tokens"
|
||||
import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useState } from "react"
|
||||
import styles from "./api-keys.module.css"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
|
||||
// need to pass in the accessToken
|
||||
const APIKeys = ({
|
||||
|
@ -20,10 +20,10 @@ const APIKeys = ({
|
|||
}: {
|
||||
tokens?: SerializedApiToken[]
|
||||
}) => {
|
||||
const session = useSession()
|
||||
const { session } = useSessionSWR()
|
||||
const { setToast } = useToasts()
|
||||
const { data, error, createToken, expireToken } = useApiTokens({
|
||||
userId: session.data?.user.id,
|
||||
userId: session?.user?.id,
|
||||
initialTokens
|
||||
})
|
||||
|
||||
|
|
|
@ -4,21 +4,27 @@ import Button from "@components/button"
|
|||
import Input from "@components/input"
|
||||
import Note from "@components/note"
|
||||
import { useToasts } from "@components/toasts"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { useSessionSWR } from "@lib/use-session-swr"
|
||||
import { useEffect, useState } from "react"
|
||||
import styles from "./profile.module.css"
|
||||
import useSWR from "swr"
|
||||
import { User } from "@prisma/client"
|
||||
|
||||
const Profile = () => {
|
||||
const { data: session } = useSession()
|
||||
const [name, setName] = useState<string>(session?.user.name || "")
|
||||
function Profile() {
|
||||
const { session } = useSessionSWR()
|
||||
console.log(session)
|
||||
const { data: userData } = useSWR<User>(
|
||||
session?.user?.id ? `/api/user/${session?.user?.id}` : null
|
||||
)
|
||||
const [name, setName] = useState<string>(userData?.displayName || "")
|
||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
||||
const { setToast } = useToasts()
|
||||
|
||||
useEffect(() => {
|
||||
if (!name) {
|
||||
setName(session?.user.name || "")
|
||||
if (!name && userData?.displayName) {
|
||||
setName(userData?.displayName)
|
||||
}
|
||||
}, [name, session])
|
||||
}, [name, userData?.displayName])
|
||||
|
||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(e.target.value)
|
||||
|
@ -39,7 +45,7 @@ const Profile = () => {
|
|||
displayName: name
|
||||
}
|
||||
|
||||
const res = await fetch(`/api/user/${session?.user.id}`, {
|
||||
const res = await fetch(`/api/user/${session?.user?.id}`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
|
@ -64,7 +70,6 @@ const Profile = () => {
|
|||
|
||||
/* if we have their email, they signed in with OAuth */
|
||||
// const imageViaOauth = Boolean(session?.user.email)
|
||||
|
||||
// const TooltipComponent = ({ children }: { children: React.ReactNode }) =>
|
||||
// imageViaOauth ? (
|
||||
// <Tooltip content="Change your profile image on your OAuth provider">
|
||||
|
@ -73,7 +78,6 @@ const Profile = () => {
|
|||
// ) : (
|
||||
// <>{children}</>
|
||||
// )
|
||||
|
||||
return (
|
||||
<>
|
||||
<Note type="warning">
|
||||
|
@ -106,39 +110,39 @@ const Profile = () => {
|
|||
/>
|
||||
</div>
|
||||
{/* <div>
|
||||
<label htmlFor="image">User Avatar</label>
|
||||
{user.image ? (
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
width={"100%"}
|
||||
placeholder="my image"
|
||||
disabled
|
||||
aria-label="Image"
|
||||
src={user.image}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon />
|
||||
)}
|
||||
<TooltipComponent>
|
||||
<div className={styles.upload}>
|
||||
<input
|
||||
type="file"
|
||||
disabled={imageViaOauth}
|
||||
className={styles.uploadInput}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={imageViaOauth}
|
||||
width="100%"
|
||||
className={styles.uploadButton}
|
||||
aria-hidden="true"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipComponent>
|
||||
</div> */}
|
||||
<label htmlFor="image">User Avatar</label>
|
||||
{user.image ? (
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
width={"100%"}
|
||||
placeholder="my image"
|
||||
disabled
|
||||
aria-label="Image"
|
||||
src={user.image}
|
||||
/>
|
||||
) : (
|
||||
<UserIcon />
|
||||
)}
|
||||
<TooltipComponent>
|
||||
<div className={styles.upload}>
|
||||
<input
|
||||
type="file"
|
||||
disabled={imageViaOauth}
|
||||
className={styles.uploadInput}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
disabled={imageViaOauth}
|
||||
width="100%"
|
||||
className={styles.uploadButton}
|
||||
aria-hidden="true"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</div>
|
||||
</TooltipComponent>
|
||||
</div> */}
|
||||
|
||||
<Button type="submit" loading={submitting}>
|
||||
Submit
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import type { GetServerSidePropsContext } from "next"
|
||||
import { unstable_getServerSession } from "next-auth/next"
|
||||
import { authOptions } from "./auth"
|
||||
|
||||
export async function getSession() {
|
||||
return await unstable_getServerSession(authOptions)
|
||||
type Params = {
|
||||
req: GetServerSidePropsContext["req"]
|
||||
res: GetServerSidePropsContext["res"]
|
||||
}
|
||||
|
||||
export async function getCurrentUser() {
|
||||
const session = await getSession()
|
||||
export async function getSession(params?: Params) {
|
||||
if (!params) return await unstable_getServerSession(authOptions)
|
||||
return await unstable_getServerSession(params.req, params.res, authOptions)
|
||||
}
|
||||
|
||||
export async function getCurrentUser(params?: Params) {
|
||||
const session = await getSession(params)
|
||||
|
||||
return session?.user
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { unstable_getServerSession } from "next-auth"
|
||||
import { authOptions } from "./auth"
|
||||
import { parseQueryParam } from "./parse-query-param"
|
||||
import { prisma } from "./prisma"
|
||||
import { getCurrentUser } from "./session"
|
||||
|
||||
/**
|
||||
* verifyApiUser checks for a `userId` param. If it exists, it checks that the
|
||||
|
@ -23,12 +22,9 @@ export const verifyApiUser = async (
|
|||
return parseAndCheckAuthToken(req)
|
||||
}
|
||||
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
if (!session) {
|
||||
return null
|
||||
}
|
||||
const user = await getCurrentUser({ req, res })
|
||||
|
||||
if (session.user.id !== userId) {
|
||||
if (user?.id !== userId) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
|
25
src/lib/use-session-swr.ts
Normal file
25
src/lib/use-session-swr.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { Session } from "next-auth"
|
||||
import useSWR from "swr"
|
||||
|
||||
export function useSessionSWR() {
|
||||
const {
|
||||
data: session,
|
||||
error,
|
||||
isLoading,
|
||||
isValidating,
|
||||
mutate
|
||||
} = useSWR<Session>("/api/auth/session")
|
||||
|
||||
return {
|
||||
session,
|
||||
error,
|
||||
isLoading,
|
||||
isValidating,
|
||||
mutate,
|
||||
/** undefined while loading */
|
||||
isAuthenticated: session?.user?.id ? true : isLoading ? undefined : false,
|
||||
/** undefined while loading */
|
||||
isAdmin:
|
||||
session?.user?.id === "admin" ? true : isLoading ? undefined : false
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { getCurrentUser } from "@lib/server/session"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { prisma } from "src/lib/server/prisma"
|
||||
import { getSession } from "next-auth/react"
|
||||
import { deleteUser } from "../user/[id]"
|
||||
|
||||
const actions = [
|
||||
|
@ -24,19 +24,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
return
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
const id = session?.user?.id
|
||||
|
||||
const isAdmin = await prisma.user
|
||||
.findUnique({
|
||||
where: {
|
||||
id
|
||||
},
|
||||
select: {
|
||||
role: true
|
||||
}
|
||||
})
|
||||
.then((user) => user?.role === "admin")
|
||||
const user = await getCurrentUser({ req, res })
|
||||
const isAdmin = user?.role === "admin"
|
||||
|
||||
if (!isAdmin) {
|
||||
return res.status(403).json({ error: "Not authorized" })
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { prisma } from "src/lib/server/prisma"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
|
||||
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { id, download } = req.query
|
||||
|
@ -30,4 +31,4 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
res.end()
|
||||
}
|
||||
|
||||
export default getRawFile
|
||||
export default withMethods(["GET"], getRawFile)
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
const handler = async (_: NextApiRequest, res: NextApiResponse) => {
|
||||
|
@ -6,4 +7,4 @@ const handler = async (_: NextApiRequest, res: NextApiResponse) => {
|
|||
})
|
||||
}
|
||||
|
||||
export default handler
|
||||
export default withMethods(["GET"], handler)
|
||||
|
|
|
@ -2,17 +2,10 @@ import { withMethods } from "@lib/api-middleware/with-methods"
|
|||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { getPostById } from "@lib/server/prisma"
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
import { getSession } from "next-auth/react"
|
||||
import { prisma } from "src/lib/server/prisma"
|
||||
import * as crypto from "crypto"
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "GET") return handleGet(req, res)
|
||||
else if (req.method === "PUT") return handlePut(req, res)
|
||||
else if (req.method === "DELETE") return handleDelete(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT", "DELETE"], handler)
|
||||
import { getSession } from "@lib/server/session"
|
||||
import { verifyApiUser } from "@lib/server/verify-api-user"
|
||||
|
||||
async function handleGet(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
||||
const id = parseQueryParam(req.query.id)
|
||||
|
@ -39,10 +32,10 @@ async function handleGet(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
res.setHeader("Cache-Control", "s-maxage=1, stale-while-revalidate")
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
const userId = await verifyApiUser(req, res)
|
||||
|
||||
// the user can always go directly to their own post
|
||||
if (session?.user.id === post.authorId) {
|
||||
if (userId === post.authorId) {
|
||||
return res.json({
|
||||
post: post,
|
||||
password: undefined
|
||||
|
@ -92,9 +85,8 @@ async function handlePut(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
|
||||
const isAuthor = session?.user.id === post.authorId
|
||||
const session = await getSession({ req, res })
|
||||
const isAuthor = session?.user?.id === post.authorId
|
||||
|
||||
if (!isAuthor) {
|
||||
return res.status(403).json({ message: "Unauthorized" })
|
||||
|
@ -142,10 +134,10 @@ async function handleDelete(
|
|||
return res.status(404).json({ message: "Post not found" })
|
||||
}
|
||||
|
||||
const session = await getSession({ req })
|
||||
const session = await getSession({ req, res })
|
||||
|
||||
const isAuthor = session?.user.id === post.authorId
|
||||
const isAdmin = session?.user.role === "admin"
|
||||
const isAuthor = session?.user?.id === post.authorId
|
||||
const isAdmin = session?.user?.role === "admin"
|
||||
|
||||
if (!isAuthor && !isAdmin) {
|
||||
return res.status(403).json({ message: "Unauthorized" })
|
||||
|
@ -162,3 +154,11 @@ async function handleDelete(
|
|||
|
||||
res.json({ message: "Post deleted" })
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
if (req.method === "GET") return handleGet(req, res)
|
||||
else if (req.method === "PUT") return handlePut(req, res)
|
||||
else if (req.method === "DELETE") return handleDelete(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT", "DELETE"], handler)
|
||||
|
|
|
@ -1,36 +1,19 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
|
||||
import { authOptions } from "@lib/server/auth"
|
||||
import { prisma } 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"
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return await handlePost(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["POST"], handler)
|
||||
import { verifyApiUser } from "@lib/server/verify-api-user"
|
||||
|
||||
async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
||||
try {
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
if (!session || !session.user.id) {
|
||||
const userId = await verifyApiUser(req, res)
|
||||
if (!userId) {
|
||||
return res.status(401).json({ error: "Unauthorized" })
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id
|
||||
}
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: "User not found" })
|
||||
}
|
||||
|
||||
const files = req.body.files as (Omit<File, "content" | "html"> & {
|
||||
content: string
|
||||
html: string
|
||||
|
@ -71,7 +54,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
password: hashedPassword,
|
||||
expiresAt: req.body.expiresAt,
|
||||
parentId: req.body.parentId,
|
||||
authorId: session.user.id,
|
||||
authorId: userId,
|
||||
files: {
|
||||
create: files.map((file) => {
|
||||
return {
|
||||
|
@ -86,7 +69,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
fileHtml[files.indexOf(file)] as string,
|
||||
"utf-8"
|
||||
),
|
||||
userId: session.user.id
|
||||
userId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -100,3 +83,9 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
|
|||
return res.status(500).json(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
return await handlePost(req, res)
|
||||
}
|
||||
|
||||
export default withMethods(["POST"], handler)
|
||||
|
|
|
@ -1,39 +1,45 @@
|
|||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import { authOptions } from "@lib/server/auth"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import { searchPosts, ServerPostWithFiles } from "@lib/server/prisma"
|
||||
import { searchPosts } from "@lib/server/prisma"
|
||||
import { verifyApiUser } from "@lib/server/verify-api-user"
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { unstable_getServerSession } from "next-auth"
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
const { q, userId } = req.query
|
||||
const query = req.query
|
||||
const q = parseQueryParam(query.q)
|
||||
const publicSearch = parseQueryParam(query.public)
|
||||
const searchQuery = parseQueryParam(q)
|
||||
|
||||
const session = await unstable_getServerSession(req, res, authOptions)
|
||||
|
||||
const query = parseQueryParam(q)
|
||||
const user = parseQueryParam(userId)
|
||||
if (!query) {
|
||||
console.log(
|
||||
"searchQuery",
|
||||
searchQuery,
|
||||
"publicSearch",
|
||||
publicSearch,
|
||||
"userId",
|
||||
query.userId
|
||||
)
|
||||
if (!searchQuery) {
|
||||
res.status(400).json({ error: "Invalid query" })
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
let posts: ServerPostWithFiles[]
|
||||
if (session?.user.id === user || session?.user.role === "admin") {
|
||||
posts = await searchPosts(query, {
|
||||
userId: user
|
||||
})
|
||||
} else {
|
||||
posts = await searchPosts(query, {
|
||||
userId: user,
|
||||
publicOnly: true
|
||||
})
|
||||
if (publicSearch) {
|
||||
const posts = await searchPosts(searchQuery)
|
||||
return res.json(posts)
|
||||
} else {
|
||||
const userId = await verifyApiUser(req, res)
|
||||
if (!userId) {
|
||||
res.status(401).json({ error: "Unauthorized" })
|
||||
return
|
||||
}
|
||||
|
||||
res.status(200).json(posts)
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
res.status(500).json({ error: "Internal server error" })
|
||||
const posts = await searchPosts(searchQuery, {
|
||||
userId,
|
||||
withFiles: true,
|
||||
publicOnly: false
|
||||
})
|
||||
|
||||
return res.json(posts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
// https://beta.nextjs.org/docs/data-fetching/revalidating#on-demand-revalidation
|
||||
|
||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import config from "@lib/config"
|
||||
import { parseQueryParam } from "@lib/server/parse-query-param"
|
||||
import type { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export default async function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
// TODO: create a new secret?
|
||||
if (req.query.secret !== config.nextauth_secret) {
|
||||
return res.status(401).json({ message: "Invalid token" })
|
||||
|
@ -26,3 +24,5 @@ export default async function handler(
|
|||
return res.status(500).send("Error revalidating")
|
||||
}
|
||||
}
|
||||
|
||||
export default withMethods(["GET"], handler)
|
||||
|
|
|
@ -58,8 +58,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||
}
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT", "DELETE"], handler)
|
||||
|
||||
/**
|
||||
* @description Deletes a user and all of their posts, files, and accounts
|
||||
* @warning This function does not perform any authorization checks
|
||||
|
@ -83,3 +81,5 @@ export async function deleteUser(id: string | undefined) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "PUT", "DELETE"], handler)
|
||||
|
|
|
@ -2,11 +2,9 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
|
|||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
import { createApiToken, prisma } from "@lib/server/prisma"
|
||||
import { verifyApiUser } from "@lib/server/verify-api-user"
|
||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
|
||||
export default async function handle(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse
|
||||
) {
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const userId = await verifyApiUser(req, res)
|
||||
if (!userId) {
|
||||
return res.status(400).json({ error: "Missing userId or auth token" })
|
||||
|
@ -55,3 +53,5 @@ export default async function handle(
|
|||
return res.status(405).end()
|
||||
}
|
||||
}
|
||||
|
||||
export default withMethods(["GET", "POST", "DELETE"], handler)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
// a nextjs api handerl
|
||||
|
||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
||||
import config from "@lib/config"
|
||||
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
|
||||
|
||||
import { NextApiRequest, NextApiResponse } from "next"
|
||||
|
||||
export const getWelcomeContent = async () => {
|
||||
export async function getWelcomeContent() {
|
||||
const introContent = config.welcome_content
|
||||
const introTitle = config.welcome_title
|
||||
|
||||
|
@ -19,7 +20,7 @@ export const getWelcomeContent = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
async function handler(_: NextApiRequest, res: NextApiResponse) {
|
||||
const welcomeContent = await getWelcomeContent()
|
||||
if (!welcomeContent) {
|
||||
return res.status(500).json({ error: "Missing welcome content" })
|
||||
|
@ -27,3 +28,5 @@ export default async function handler(_: NextApiRequest, res: NextApiResponse) {
|
|||
|
||||
return res.json(welcomeContent)
|
||||
}
|
||||
|
||||
export default withMethods(["GET"], handler)
|
||||
|
|
Loading…
Reference in a new issue