From ba732dcd713ab4d953dc351951ed6b98a3c470e7 Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Thu, 12 Jan 2023 20:50:56 -0800 Subject: [PATCH] refactor to SWR and verifyApiUser; personal post search is broken --- .eslintrc.json | 19 +-- next.config.mjs | 3 + src/app/(auth)/components/index.tsx | 14 +- src/app/(auth)/signup/page.tsx | 2 +- .../components/file-dropdown/index.tsx | 7 +- src/app/(posts)/components/preview/index.tsx | 28 ++-- src/app/(posts)/components/tabs/index.tsx | 4 +- .../new/components/description/index.tsx | 5 +- .../edit-document/formatting-icons/index.tsx | 13 +- .../edit-document/index.tsx | 10 +- .../components/edit-document-list/index.tsx | 8 +- src/app/(posts)/new/components/new.tsx | 122 ++++++++++-------- .../(posts)/new/components/title/index.tsx | 5 +- src/app/(posts)/new/from/[id]/page.tsx | 4 +- src/app/(posts)/new/page.tsx | 6 +- .../post-files/password-modal-wrapper.tsx | 25 ++-- src/app/admin/components/tables.tsx | 3 +- .../badges/visibility-control/index.tsx | 22 ++-- src/app/components/header/index.tsx | 16 +-- src/app/components/post-list/index.tsx | 19 +-- src/app/components/post-list/list-item.tsx | 2 +- src/app/hooks/swr/use-api-tokens.ts | 12 +- src/app/layout.tsx | 3 +- src/app/lib/fetch-with-user.ts | 12 ++ src/app/page.tsx | 2 +- src/app/providers.tsx | 24 +++- .../settings/components/sections/api-keys.tsx | 6 +- .../settings/components/sections/profile.tsx | 90 +++++++------ src/lib/server/session.ts | 15 ++- src/lib/server/verify-api-user.ts | 10 +- src/lib/use-session-swr.ts | 25 ++++ src/pages/api/admin/index.ts | 17 +-- src/pages/api/file/raw/[id].ts | 3 +- src/pages/api/health.ts | 3 +- src/pages/api/post/[id].ts | 34 ++--- src/pages/api/post/index.ts | 33 ++--- src/pages/api/post/search.ts | 54 ++++---- src/pages/api/revalidate.ts | 8 +- src/pages/api/user/[id].ts | 4 +- src/pages/api/user/tokens.ts | 8 +- src/pages/api/welcome.ts | 7 +- 41 files changed, 379 insertions(+), 328 deletions(-) create mode 100644 src/app/lib/fetch-with-user.ts create mode 100644 src/lib/use-session-swr.ts diff --git a/.eslintrc.json b/.eslintrc.json index 93ac1eb2..839b6a61 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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" + } } diff --git a/next.config.mjs b/next.config.mjs index f4afec3b..6ad702a7 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -23,6 +23,9 @@ const nextConfig = { }, images: { domains: ["avatars.githubusercontent.com"] + }, + env: { + NEXT_PUBLIC_DRIFT_URL: process.env.VERCEL_URL || process.env.DRIFT_URL } } diff --git a/src/app/(auth)/components/index.tsx b/src/app/(auth)/components/index.tsx index cb65cebd..065ac078 100644 --- a/src/app/(auth)/components/index.tsx +++ b/src/app/(auth)/components/index.tsx @@ -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) => { + async function handleSubmit(event: React.FormEvent) { event.preventDefault() const res = await signIn("credentials", { @@ -62,17 +62,17 @@ const Auth = ({ } } - const handleChangeUsername = (event: React.ChangeEvent) => { + function handleChangeUsername(event: React.ChangeEvent) { setUsername(event.target.value) } - const handleChangePassword = (event: React.ChangeEvent) => { + function handleChangePassword(event: React.ChangeEvent) { setPassword(event.target.value) } - const handleChangeServerPassword = ( + function handleChangeServerPassword( event: React.ChangeEvent - ) => { + ) { setServerPassword(event.target.value) } diff --git a/src/app/(auth)/signup/page.tsx b/src/app/(auth)/signup/page.tsx index dc82bbc7..6e758563 100644 --- a/src/app/(auth)/signup/page.tsx +++ b/src/app/(auth)/signup/page.tsx @@ -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() } diff --git a/src/app/(posts)/components/file-dropdown/index.tsx b/src/app/(posts)/components/file-dropdown/index.tsx index 7c028157..ddf6ee4e 100644 --- a/src/app/(posts)/components/file-dropdown/index.tsx +++ b/src/app/(posts)/components/file-dropdown/index.tsx @@ -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["files"] loading?: boolean -}) => { +}) { if (loading) { return ( diff --git a/src/app/(posts)/components/preview/index.tsx b/src/app/(posts)/components/preview/index.tsx index cdda5bd4..cc5d914c 100644 --- a/src/app/(posts)/components/preview/index.tsx +++ b/src/app/(posts)/components/preview/index.tsx @@ -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(content) const [isLoading, setIsLoading] = useState(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 (
+ }} /> ) } diff --git a/src/app/(posts)/components/tabs/index.tsx b/src/app/(posts)/components/tabs/index.tsx index 6f5a0f43..98f5c055 100644 --- a/src/app/(posts)/components/tabs/index.tsx +++ b/src/app/(posts)/components/tabs/index.tsx @@ -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) => void - onPaste?: (e: any) => void + onPaste?: (e: ClipboardEvent) => void title?: string content?: string preview?: string diff --git a/src/app/(posts)/new/components/description/index.tsx b/src/app/(posts)/new/components/description/index.tsx index bd62b237..7d668d58 100644 --- a/src/app/(posts)/new/components/description/index.tsx +++ b/src/app/(posts)/new/components/description/index.tsx @@ -8,7 +8,7 @@ type props = { description: string } -const Description = ({ onChange, description }: props) => { +function Description({ onChange, description }: props) { return (
{ label="Description" maxLength={256} width="100%" - placeholder="An optional description of your post" - /> + placeholder="An optional description of your post" />
) } diff --git a/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx b/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx index f48c418e..76504b93 100644 --- a/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx +++ b/src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons/index.tsx @@ -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 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: , @@ -83,8 +81,7 @@ const FormattingIcons = ({ iconRight={icon} onMouseDown={(e) => e.preventDefault()} onClick={action} - buttonType="secondary" - /> + buttonType="secondary" /> ))} diff --git a/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx b/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx index 0395537a..bae77b75 100644 --- a/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx +++ b/src/app/(posts)/new/components/edit-document-list/edit-document/index.tsx @@ -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) => void defaultTab?: "edit" | "preview" remove?: () => void - onPaste?: (e: any) => void + onPaste?: (e: ClipboardEvent) => 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) => 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} diff --git a/src/app/(posts)/new/components/edit-document-list/index.tsx b/src/app/(posts)/new/components/edit-document-list/index.tsx index f22abb4b..6b37d7cc 100644 --- a/src/app/(posts)/new/components/edit-document-list/index.tsx +++ b/src/app/(posts)/new/components/edit-document-list/index.tsx @@ -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) => void +}) { const handleOnChange = useCallback( (i: number) => (e: ChangeEvent) => { updateDocContent(i)(e.target.value) diff --git a/src/app/(posts)/new/components/new.tsx b/src/app/(posts)/new/components/new.tsx index 0a97a97d..73ee1e4b 100644 --- a/src/app/(posts)/new/components/new.tsx +++ b/src/app/(posts)/new/components/new.tsx @@ -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) => { 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) { 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 - }) => ( - { - 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 (
@@ -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 + /> + ) +} diff --git a/src/app/(posts)/new/components/title/index.tsx b/src/app/(posts)/new/components/title/index.tsx index fbc3714f..35e2a8ba 100644 --- a/src/app/(posts)/new/components/title/index.tsx +++ b/src/app/(posts)/new/components/title/index.tsx @@ -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> ) } diff --git a/src/app/(posts)/new/from/[id]/page.tsx b/src/app/(posts)/new/from/[id]/page.tsx index 6f4e2a39..e7c2562a 100644 --- a/src/app/(posts)/new/from/[id]/page.tsx +++ b/src/app/(posts)/new/from/[id]/page.tsx @@ -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") diff --git a/src/app/(posts)/new/page.tsx b/src/app/(posts)/new/page.tsx index 8bd3712c..b2f18bf6 100644 --- a/src/app/(posts)/new/page.tsx +++ b/src/app/(posts)/new/page.tsx @@ -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" diff --git a/src/app/(posts)/post/[id]/components/post-files/password-modal-wrapper.tsx b/src/app/(posts)/post/[id]/components/post-files/password-modal-wrapper.tsx index 1e0970f3..bd6560ad 100644 --- a/src/app/(posts)/post/[id]/components/post-files/password-modal-wrapper.tsx +++ b/src/app/(posts)/post/[id]/components/post-files/password-modal-wrapper.tsx @@ -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({ diff --git a/src/app/admin/components/tables.tsx b/src/app/admin/components/tables.tsx index 571dbfee..782d9a46 100644 --- a/src/app/admin/components/tables.tsx +++ b/src/app/admin/components/tables.tsx @@ -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" diff --git a/src/app/components/badges/visibility-control/index.tsx b/src/app/components/badges/visibility-control/index.tsx index 382464f7..4f9128fd 100644 --- a/src/app/components/badges/visibility-control/index.tsx +++ b/src/app/components/badges/visibility-control/index.tsx @@ -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() diff --git a/src/app/components/header/index.tsx b/src/app/components/header/index.tsx index 2819b861..86f08c8f 100644 --- a/src/app/components/header/index.tsx +++ b/src/app/components/header/index.tsx @@ -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) diff --git a/src/app/components/post-list/index.tsx b/src/app/components/post-list/index.tsx index d83871b9..382a0263 100644 --- a/src/app/components/post-list/index.tsx +++ b/src/app/components/post-list/index.tsx @@ -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> ) } diff --git a/src/app/components/post-list/list-item.tsx b/src/app/components/post-list/list-item.tsx index 1d4becc3..1f83d83e 100644 --- a/src/app/components/post-list/list-item.tsx +++ b/src/app/components/post-list/list-item.tsx @@ -66,7 +66,7 @@ const ListItem = ({ } return ( - <FadeIn as="li" key={post.id}> + <FadeIn key={post.id}> <li> <Card style={{ overflowY: "scroll" }}> <> diff --git a/src/app/hooks/swr/use-api-tokens.ts b/src/app/hooks/swr/use-api-tokens.ts index 3d418db3..7f8619ad 100644 --- a/src/app/hooks/swr/use-api-tokens.ts +++ b/src/app/hooks/swr/use-api-tokens.ts @@ -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 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8c35e5cf..d2b624c1 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -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 /> diff --git a/src/app/lib/fetch-with-user.ts b/src/app/lib/fetch-with-user.ts new file mode 100644 index 00000000..81094c03 --- /dev/null +++ b/src/app/lib/fetch-with-user.ts @@ -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) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 2332dfa7..cc19b1de 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 9fb9d376..c7a645ce 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -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> ) } diff --git a/src/app/settings/components/sections/api-keys.tsx b/src/app/settings/components/sections/api-keys.tsx index b69d7016..dc1fb919 100644 --- a/src/app/settings/components/sections/api-keys.tsx +++ b/src/app/settings/components/sections/api-keys.tsx @@ -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 }) diff --git a/src/app/settings/components/sections/profile.tsx b/src/app/settings/components/sections/profile.tsx index c579bd69..d716e150 100644 --- a/src/app/settings/components/sections/profile.tsx +++ b/src/app/settings/components/sections/profile.tsx @@ -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 diff --git a/src/lib/server/session.ts b/src/lib/server/session.ts index d7ab8c6b..a650f025 100644 --- a/src/lib/server/session.ts +++ b/src/lib/server/session.ts @@ -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 } diff --git a/src/lib/server/verify-api-user.ts b/src/lib/server/verify-api-user.ts index 10193d1d..f253b49c 100644 --- a/src/lib/server/verify-api-user.ts +++ b/src/lib/server/verify-api-user.ts @@ -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 } diff --git a/src/lib/use-session-swr.ts b/src/lib/use-session-swr.ts new file mode 100644 index 00000000..dd56bd4b --- /dev/null +++ b/src/lib/use-session-swr.ts @@ -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 + } +} diff --git a/src/pages/api/admin/index.ts b/src/pages/api/admin/index.ts index 63c4d45b..115a4738 100644 --- a/src/pages/api/admin/index.ts +++ b/src/pages/api/admin/index.ts @@ -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" }) diff --git a/src/pages/api/file/raw/[id].ts b/src/pages/api/file/raw/[id].ts index ec2dd564..4a9de371 100644 --- a/src/pages/api/file/raw/[id].ts +++ b/src/pages/api/file/raw/[id].ts @@ -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) diff --git a/src/pages/api/health.ts b/src/pages/api/health.ts index 95da3084..7200f941 100644 --- a/src/pages/api/health.ts +++ b/src/pages/api/health.ts @@ -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) diff --git a/src/pages/api/post/[id].ts b/src/pages/api/post/[id].ts index 89e0cc3b..0ec93513 100644 --- a/src/pages/api/post/[id].ts +++ b/src/pages/api/post/[id].ts @@ -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) diff --git a/src/pages/api/post/index.ts b/src/pages/api/post/index.ts index 6f03182a..eb6b9cc3 100644 --- a/src/pages/api/post/index.ts +++ b/src/pages/api/post/index.ts @@ -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) diff --git a/src/pages/api/post/search.ts b/src/pages/api/post/search.ts index f71cd8dc..129c5c9b 100644 --- a/src/pages/api/post/search.ts +++ b/src/pages/api/post/search.ts @@ -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) } } diff --git a/src/pages/api/revalidate.ts b/src/pages/api/revalidate.ts index 7001ed62..133e3d98 100644 --- a/src/pages/api/revalidate.ts +++ b/src/pages/api/revalidate.ts @@ -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) diff --git a/src/pages/api/user/[id].ts b/src/pages/api/user/[id].ts index c60faf03..c30c500e 100644 --- a/src/pages/api/user/[id].ts +++ b/src/pages/api/user/[id].ts @@ -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) diff --git a/src/pages/api/user/tokens.ts b/src/pages/api/user/tokens.ts index d4145268..dc7531a6 100644 --- a/src/pages/api/user/tokens.ts +++ b/src/pages/api/user/tokens.ts @@ -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) diff --git a/src/pages/api/welcome.ts b/src/pages/api/welcome.ts index bc640747..18d4015b 100644 --- a/src/pages/api/welcome.ts +++ b/src/pages/api/welcome.ts @@ -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)