refactor to SWR and verifyApiUser; personal post search is broken

This commit is contained in:
Max Leiter 2023-01-12 20:50:56 -08:00
parent 6fb81d77b9
commit ba732dcd71
41 changed files with 379 additions and 328 deletions

View file

@ -1,14 +1,9 @@
{ {
"extends": [ "plugins": ["@typescript-eslint"],
"next/core-web-vitals", "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"],
"eslint:recommended", "ignorePatterns": ["node_modules/", "__tests__/"],
"plugin:@typescript-eslint/recommended" "rules": {
], "@typescript-eslint/no-unused-vars": "error",
"parser": "@typescript-eslint/parser", "@typescript-eslint/no-explicit-any": "error"
"plugins": ["@typescript-eslint"], }
"root": true,
"ignorePatterns": ["node_modules/", "__tests__/"],
"rules": {
"no-mixed-spaces-and-tabs": ["off"]
}
} }

View file

@ -23,6 +23,9 @@ const nextConfig = {
}, },
images: { images: {
domains: ["avatars.githubusercontent.com"] domains: ["avatars.githubusercontent.com"]
},
env: {
NEXT_PUBLIC_DRIFT_URL: process.env.VERCEL_URL || process.env.DRIFT_URL
} }
} }

View file

@ -11,7 +11,7 @@ import { useToasts } from "@components/toasts"
import { useRouter, useSearchParams } from "next/navigation" import { useRouter, useSearchParams } from "next/navigation"
import Note from "@components/note" import Note from "@components/note"
const Auth = ({ function Auth({
page, page,
requiresServerPassword, requiresServerPassword,
isGithubEnabled isGithubEnabled
@ -19,7 +19,7 @@ const Auth = ({
page: "signup" | "signin" page: "signup" | "signin"
requiresServerPassword?: boolean requiresServerPassword?: boolean
isGithubEnabled?: boolean isGithubEnabled?: boolean
}) => { }) {
const [serverPassword, setServerPassword] = useState("") const [serverPassword, setServerPassword] = useState("")
const { setToast } = useToasts() const { setToast } = useToasts()
const signingIn = page === "signin" const signingIn = page === "signin"
@ -38,7 +38,7 @@ const Auth = ({
} }
}, [queryParams, setToast]) }, [queryParams, setToast])
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => { async function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault() event.preventDefault()
const res = await signIn("credentials", { 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) setUsername(event.target.value)
} }
const handleChangePassword = (event: React.ChangeEvent<HTMLInputElement>) => { function handleChangePassword(event: React.ChangeEvent<HTMLInputElement>) {
setPassword(event.target.value) setPassword(event.target.value)
} }
const handleChangeServerPassword = ( function handleChangeServerPassword(
event: React.ChangeEvent<HTMLInputElement> event: React.ChangeEvent<HTMLInputElement>
) => { ) {
setServerPassword(event.target.value) setServerPassword(event.target.value)
} }

View file

@ -2,7 +2,7 @@ import Auth from "../components"
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode" import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
import config from "@lib/config" import config from "@lib/config"
const getPasscode = async () => { async function getPasscode() {
return await getRequiresPasscode() return await getRequiresPasscode()
} }

View file

@ -8,13 +8,12 @@ import { ChevronDown, Code, File as FileIcon } from "react-feather"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
import Link from "next/link" import Link from "next/link"
const FileDropdown = ({ function FileDropdown({
files, files, loading
loading
}: { }: {
files: Pick<PostWithFiles, "files">["files"] files: Pick<PostWithFiles, "files">["files"]
loading?: boolean loading?: boolean
}) => { }) {
if (loading) { if (loading) {
return ( return (
<Popover> <Popover>

View file

@ -3,6 +3,7 @@ import styles from "./preview.module.css"
import "@styles/markdown.css" import "@styles/markdown.css"
import "@styles/syntax.css" import "@styles/syntax.css"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
type Props = { type Props = {
height?: number | string height?: number | string
@ -11,12 +12,9 @@ type Props = {
title?: string title?: string
} }
const MarkdownPreview = ({ function MarkdownPreview({
height = 500, height = 500, fileId, content = "", title
fileId, }: Props) {
content = "",
title
}: Props) => {
const [preview, setPreview] = useState<string>(content) const [preview, setPreview] = useState<string>(content)
const [isLoading, setIsLoading] = useState<boolean>(true) const [isLoading, setIsLoading] = useState<boolean>(true)
useEffect(() => { useEffect(() => {
@ -27,11 +25,11 @@ const MarkdownPreview = ({
const body = fileId const body = fileId
? undefined ? undefined
: JSON.stringify({ : JSON.stringify({
title: title || "", title: title || "",
content: content content: content
}) })
const resp = await fetch(path, { const resp = await fetchWithUser(path, {
method: method, method: method,
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -62,20 +60,18 @@ const MarkdownPreview = ({
export default memo(MarkdownPreview) export default memo(MarkdownPreview)
export const StaticPreview = ({ export function StaticPreview({
preview, preview, height = 500
height = 500
}: { }: {
preview: string preview: string
height: string | number height: string | number
}) => { }) {
return ( return (
<article <article
className={styles.markdownPreview} className={styles.markdownPreview}
dangerouslySetInnerHTML={{ __html: preview }} dangerouslySetInnerHTML={{ __html: preview }}
style={{ style={{
height height
}} }} />
/>
) )
} }

View file

@ -2,7 +2,7 @@
import * as RadixTabs from "@radix-ui/react-tabs" import * as RadixTabs from "@radix-ui/react-tabs"
import FormattingIcons from "src/app/(posts)/new/components/edit-document-list/edit-document/formatting-icons" 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 TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor"
import Preview, { StaticPreview } from "../preview" import Preview, { StaticPreview } from "../preview"
import styles from "./tabs.module.css" import styles from "./tabs.module.css"
@ -11,7 +11,7 @@ type Props = RadixTabs.TabsProps & {
isEditing: boolean isEditing: boolean
defaultTab: "preview" | "edit" defaultTab: "preview" | "edit"
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
onPaste?: (e: any) => void onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
title?: string title?: string
content?: string content?: string
preview?: string preview?: string

View file

@ -8,7 +8,7 @@ type props = {
description: string description: string
} }
const Description = ({ onChange, description }: props) => { function Description({ onChange, description }: props) {
return ( return (
<div className={styles.description}> <div className={styles.description}>
<Input <Input
@ -17,8 +17,7 @@ const Description = ({ onChange, description }: props) => {
label="Description" label="Description"
maxLength={256} maxLength={256}
width="100%" width="100%"
placeholder="An optional description of your post" placeholder="An optional description of your post" />
/>
</div> </div>
) )
} }

View file

@ -14,21 +14,19 @@ import Button from "@components/button"
import clsx from "clsx" import clsx from "clsx"
// TODO: clean up // TODO: clean up
const FormattingIcons = ({ function FormattingIcons({
textareaRef, textareaRef, className
className
}: { }: {
textareaRef?: RefObject<TextareaMarkdownRef> textareaRef?: RefObject<TextareaMarkdownRef>
className?: string className?: string
}) => { }) {
const formattingActions = useMemo(() => { const formattingActions = useMemo(() => {
const handleBoldClick = () => textareaRef?.current?.trigger("bold") const handleBoldClick = () => textareaRef?.current?.trigger("bold")
const handleItalicClick = () => textareaRef?.current?.trigger("italic") const handleItalicClick = () => textareaRef?.current?.trigger("italic")
const handleLinkClick = () => textareaRef?.current?.trigger("link") const handleLinkClick = () => textareaRef?.current?.trigger("link")
const handleImageClick = () => textareaRef?.current?.trigger("image") const handleImageClick = () => textareaRef?.current?.trigger("image")
const handleCodeClick = () => textareaRef?.current?.trigger("code") const handleCodeClick = () => textareaRef?.current?.trigger("code")
const handleListClick = () => const handleListClick = () => textareaRef?.current?.trigger("unordered-list")
textareaRef?.current?.trigger("unordered-list")
return [ return [
{ {
icon: <Bold />, icon: <Bold />,
@ -83,8 +81,7 @@ const FormattingIcons = ({
iconRight={icon} iconRight={icon}
onMouseDown={(e) => e.preventDefault()} onMouseDown={(e) => e.preventDefault()}
onClick={action} onClick={action}
buttonType="secondary" buttonType="secondary" />
/>
</Tooltip> </Tooltip>
))} ))}
</div> </div>

View file

@ -1,4 +1,4 @@
import { ChangeEvent, useCallback } from "react" import { ChangeEvent, ClipboardEvent, useCallback } from "react"
import styles from "./document.module.css" import styles from "./document.module.css"
import Button from "@components/button" import Button from "@components/button"
import Input from "@components/input" import Input from "@components/input"
@ -12,10 +12,10 @@ type Props = {
handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void handleOnContentChange?: (e: ChangeEvent<HTMLTextAreaElement>) => void
defaultTab?: "edit" | "preview" defaultTab?: "edit" | "preview"
remove?: () => void remove?: () => void
onPaste?: (e: any) => void onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
} }
const Document = ({ function Document({
onPaste, onPaste,
remove, remove,
title, title,
@ -23,7 +23,7 @@ const Document = ({
setTitle, setTitle,
defaultTab = "edit", defaultTab = "edit",
handleOnContentChange handleOnContentChange
}: Props) => { }: Props) {
const onTitleChange = useCallback( const onTitleChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => (event: ChangeEvent<HTMLInputElement>) =>
setTitle ? setTitle(event.target.value) : null, setTitle ? setTitle(event.target.value) : null,
@ -84,6 +84,8 @@ const Document = ({
isEditing={true} isEditing={true}
defaultTab={defaultTab} defaultTab={defaultTab}
handleOnContentChange={handleOnContentChange} 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} onPaste={onPaste}
title={title} title={title}
content={content} content={content}

View file

@ -1,8 +1,8 @@
import type { Document } from "../new" import type { Document } from "../new"
import DocumentComponent from "./edit-document" import DocumentComponent from "./edit-document"
import { ChangeEvent, useCallback } from "react" import { ChangeEvent, useCallback, ClipboardEvent } from "react"
const DocumentList = ({ function DocumentList({
docs, docs,
removeDoc, removeDoc,
updateDocContent, updateDocContent,
@ -13,8 +13,8 @@ const DocumentList = ({
updateDocTitle: (i: number) => (title: string) => void updateDocTitle: (i: number) => (title: string) => void
updateDocContent: (i: number) => (content: string) => void updateDocContent: (i: number) => (content: string) => void
removeDoc: (i: number) => () => void removeDoc: (i: number) => () => void
onPaste: (e: any) => void onPaste?: (e: ClipboardEvent<HTMLTextAreaElement>) => void
}) => { }) {
const handleOnChange = useCallback( const handleOnChange = useCallback(
(i: number) => (e: ChangeEvent<HTMLTextAreaElement>) => { (i: number) => (e: ChangeEvent<HTMLTextAreaElement>) => {
updateDocContent(i)(e.target.value) updateDocContent(i)(e.target.value)

View file

@ -1,7 +1,7 @@
"use client" "use client"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useCallback, useState } from "react" import { useCallback, useState, ClipboardEvent } from "react"
import generateUUID from "@lib/generate-uuid" import generateUUID from "@lib/generate-uuid"
import styles from "./post.module.css" import styles from "./post.module.css"
import EditDocumentList from "./edit-document-list" import EditDocumentList from "./edit-document-list"
@ -17,7 +17,8 @@ import Button from "@components/button"
import Input from "@components/input" import Input from "@components/input"
import ButtonDropdown from "@components/button-dropdown" import ButtonDropdown from "@components/button-dropdown"
import { useToasts } from "@components/toasts" 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 = { const emptyDoc = {
title: "", title: "",
@ -31,14 +32,14 @@ export type Document = {
id: string id: string
} }
const Post = ({ function Post({
initialPost: stringifiedInitialPost, initialPost: stringifiedInitialPost,
newPostParent newPostParent
}: { }: {
initialPost?: string initialPost?: string
newPostParent?: string newPostParent?: string
}) => { }): JSX.Element | null {
const session = useSession() const { isAuthenticated } = useSessionSWR()
const parsedPost = JSON.parse(stringifiedInitialPost || "{}") as PostWithFiles const parsedPost = JSON.parse(stringifiedInitialPost || "{}") as PostWithFiles
const initialPost = parsedPost?.id ? parsedPost : null const initialPost = parsedPost?.id ? parsedPost : null
@ -74,7 +75,7 @@ const Post = ({
parentId?: string parentId?: string
} }
) => { ) => {
const res = await fetch(url, { const res = await fetchWithUser(url, {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -115,7 +116,6 @@ const Post = ({
} }
setPasswordModalVisible(false) setPasswordModalVisible(false)
setSubmitting(true) setSubmitting(true)
let hasErrored = false let hasErrored = false
@ -164,15 +164,6 @@ const Post = ({
[docs, expiresAt, newPostParent, sendRequest, setToast, title] [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>) => { const onChangeTitle = useCallback((e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault() e.preventDefault()
setTitle(e.target.value) setTitle(e.target.value)
@ -186,28 +177,47 @@ const Post = ({
[] []
) )
if (session.status === "unauthenticated") { if (isAuthenticated === false) {
router.push("/signin") router.push("/signin")
return null return null
} }
const updateDocTitle = (i: number) => (title: string) => { function onClosePasswordModal() {
setDocs((docs) => setPasswordModalVisible(false)
docs.map((doc, index) => (i === index ? { ...doc, title } : doc)) setSubmitting(false)
)
} }
const updateDocContent = (i: number) => (content: string) => { function submitPassword(password: string) {
setDocs((docs) => return onSubmit("protected", password)
docs.map((doc, index) => (i === index ? { ...doc, content } : doc))
)
} }
const removeDoc = (i: number) => () => { function onChangeExpiration(date: Date) {
setDocs((docs) => docs.filter((_, index) => i !== index)) 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, // if no title is set and the only document is empty,
const isFirstDocEmpty = const isFirstDocEmpty =
docs.length <= 1 && (docs.length ? docs[0].title === "" : true) docs.length <= 1 && (docs.length ? docs[0].title === "" : true)
@ -224,7 +234,7 @@ const Post = ({
else setDocs((docs) => [...docs, ...files]) else setDocs((docs) => [...docs, ...files])
} }
const onPaste = (e: ClipboardEvent) => { function onPaste(e: ClipboardEvent<HTMLTextAreaElement>) {
const pastedText = e.clipboardData?.getData("text") const pastedText = e.clipboardData?.getData("text")
if (pastedText) { 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 ( return (
<div className={styles.root}> <div className={styles.root}>
<Title title={title} onChange={onChangeTitle} /> <Title title={title} onChange={onChangeTitle} />
@ -345,3 +329,31 @@ const Post = ({
} }
export default 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
/>
)
}

View file

@ -20,7 +20,7 @@ type props = {
title?: string title?: string
} }
const Title = ({ onChange, title }: props) => { function Title({ onChange, title }: props) {
return ( return (
<div className={styles.title}> <div className={styles.title}>
<h1 style={{ margin: 0, padding: 0 }}>Drift</h1> <h1 style={{ margin: 0, padding: 0 }}>Drift</h1>
@ -31,8 +31,7 @@ const Title = ({ onChange, title }: props) => {
label="Title" label="Title"
className={styles.labelAndInput} className={styles.labelAndInput}
style={{ width: "100%" }} style={{ width: "100%" }}
labelClassName={styles.labelAndInput} labelClassName={styles.labelAndInput} />
/>
</div> </div>
) )
} }

View file

@ -3,13 +3,13 @@ import { notFound, redirect } from "next/navigation"
import { getPostById } from "@lib/server/prisma" import { getPostById } from "@lib/server/prisma"
import { getSession } from "@lib/server/session" import { getSession } from "@lib/server/session"
const NewFromExisting = async ({ async function NewFromExisting({
params params
}: { }: {
params: { params: {
id: string id: string
} }
}) => { }) {
const session = await getSession() const session = await getSession()
if (!session?.user) { if (!session?.user) {
return redirect("/signin") return redirect("/signin")

View file

@ -1,8 +1,8 @@
import NewPost from "src/app/(posts)/new/components/new" import NewPost from "src/app/(posts)/new/components/new"
import "./react-datepicker.css" import "./react-datepicker.css"
const New = () => <NewPost /> export default function New() {
return <NewPost />
export default New }
export const dynamic = "force-static" export const dynamic = "force-static"

View file

@ -5,7 +5,8 @@ import PasswordModal from "@components/password-modal"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useCallback, useEffect, useState } from "react" import { useCallback, useEffect, useState } from "react"
import { useToasts } from "@components/toasts" 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 = { type Props = {
setPost: (post: PostWithFilesAndAuthor) => void setPost: (post: PostWithFilesAndAuthor) => void
@ -16,20 +17,22 @@ type Props = {
const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => { const PasswordModalWrapper = ({ setPost, postId, authorId }: Props) => {
const router = useRouter() const router = useRouter()
const { setToast } = useToasts() const { setToast } = useToasts()
const { data: session, status } = useSession() const { session, isLoading } = useSessionSWR()
const isAuthor = const isAuthor = isLoading
status === "loading" ? undefined
? undefined : session?.user && session?.user?.id === authorId
: session?.user && session?.user?.id === authorId
const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false)
const onSubmit = useCallback( const onSubmit = useCallback(
async (password: string) => { async (password: string) => {
const res = await fetch(`/api/post/${postId}?password=${password}`, { const res = await fetchWithUser(
method: "GET", `/api/post/${postId}?password=${password}`,
headers: { {
"Content-Type": "application/json" method: "GET",
headers: {
"Content-Type": "application/json"
}
} }
}) )
if (!res.ok) { if (!res.ok) {
setToast({ setToast({

View file

@ -6,6 +6,7 @@ import { useToasts } from "@components/toasts"
import { Post, User } from "@lib/server/prisma" import { Post, User } from "@lib/server/prisma"
import Link from "next/link" import Link from "next/link"
import { useState } from "react" import { useState } from "react"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
import styles from "./table.module.css" import styles from "./table.module.css"
export function UserTable({ export function UserTable({
@ -25,7 +26,7 @@ export function UserTable({
const deleteUser = async (id: string) => { const deleteUser = async (id: string) => {
try { try {
const res = await fetch("/api/admin?action=delete-user", { const res = await fetchWithUser("/api/admin?action=delete-user", {
method: "DELETE", method: "DELETE",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"

View file

@ -6,8 +6,9 @@ import ButtonGroup from "@components/button-group"
import Button from "@components/button" import Button from "@components/button"
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { Spinner } from "@components/spinner" import { Spinner } from "@components/spinner"
import { useSession } from "next-auth/react"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { useSessionSWR } from "@lib/use-session-swr"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
type Props = { type Props = {
authorId: string authorId: string
@ -20,7 +21,7 @@ const VisibilityControl = ({
postId, postId,
visibility: postVisibility visibility: postVisibility
}: Props) => { }: Props) => {
const { data: session } = useSession() const { session } = useSessionSWR()
const isAuthor = session?.user && session?.user?.id === authorId const isAuthor = session?.user && session?.user?.id === authorId
const [visibility, setVisibility] = useState<string>(postVisibility) const [visibility, setVisibility] = useState<string>(postVisibility)
@ -31,13 +32,16 @@ const VisibilityControl = ({
const sendRequest = useCallback( const sendRequest = useCallback(
async (visibility: string, password?: string) => { async (visibility: string, password?: string) => {
const res = await fetch(`/api/post/${postId}`, { const res = await fetchWithUser(
method: "PUT", `/api/post/${postId}`,
headers: { {
"Content-Type": "application/json" method: "PUT",
}, headers: {
body: JSON.stringify({ visibility, password }) "Content-Type": "application/json"
}) },
body: JSON.stringify({ visibility, password })
}
)
if (res.ok) { if (res.ok) {
const json = await res.json() const json = await res.json()

View file

@ -4,7 +4,7 @@ import styles from "./header.module.css"
// import useUserData from "@lib/hooks/use-user-data" // import useUserData from "@lib/hooks/use-user-data"
import Link from "@components/link" import Link from "@components/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
import { signOut, useSession } from "next-auth/react" import { signOut } from "next-auth/react"
import Button from "@components/button" import Button from "@components/button"
import clsx from "clsx" import clsx from "clsx"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
@ -23,6 +23,7 @@ import {
import * as DropdownMenu from "@radix-ui/react-dropdown-menu" import * as DropdownMenu from "@radix-ui/react-dropdown-menu"
import buttonStyles from "@components/button/button.module.css" import buttonStyles from "@components/button/button.module.css"
import { useMemo } from "react" import { useMemo } from "react"
import { useSessionSWR } from "@lib/use-session-swr"
type Tab = { type Tab = {
name: string name: string
@ -33,10 +34,7 @@ type Tab = {
} }
const Header = () => { const Header = () => {
const session = useSession() const { isAuthenticated, isAdmin, isLoading, mutate } = useSessionSWR()
const isSignedIn = session?.status === "authenticated"
const isAdmin = session?.data?.user?.role === "admin"
const isLoading = session?.status === "loading"
const pathname = usePathname() const pathname = usePathname()
const { setTheme, resolvedTheme } = useTheme() const { setTheme, resolvedTheme } = useTheme()
@ -97,7 +95,7 @@ const Header = () => {
value: "theme" value: "theme"
}) })
if (isSignedIn) if (isAuthenticated)
return [ return [
{ {
name: "New", name: "New",
@ -122,10 +120,12 @@ const Header = () => {
name: "Sign Out", name: "Sign Out",
icon: <UserX />, icon: <UserX />,
value: "signout", value: "signout",
onClick: () => onClick: () => {
mutate(undefined)
signOut({ signOut({
callbackUrl: "/" callbackUrl: "/"
}) })
}
} }
] ]
else else
@ -150,7 +150,7 @@ const Header = () => {
href: "/signup" href: "/signup"
} }
] ]
}, [isAdmin, resolvedTheme, isSignedIn, setTheme]) }, [isAdmin, resolvedTheme, isAuthenticated, setTheme, mutate])
const buttons = pages.map(getButton) const buttons = pages.map(getButton)

View file

@ -9,6 +9,7 @@ import { useToasts } from "@components/toasts"
import { ListItemSkeleton } from "./list-item-skeleton" import { ListItemSkeleton } from "./list-item-skeleton"
import Link from "@components/link" import Link from "@components/link"
import debounce from "lodash.debounce" import debounce from "lodash.debounce"
import { fetchWithUser } from "src/app/lib/fetch-with-user"
type Props = { type Props = {
initialPosts: string | PostWithFiles[] initialPosts: string | PostWithFiles[]
@ -36,6 +37,7 @@ const PostList = ({
const [searchValue, setSearchValue] = useState("") const [searchValue, setSearchValue] = useState("")
const [searching, setSearching] = useState(false) const [searching, setSearching] = useState(false)
const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts) const [posts, setPosts] = useState<PostWithFiles[]>(initialPosts)
const { setToast } = useToasts() const { setToast } = useToasts()
const showSkeleton = skeleton || searching const showSkeleton = skeleton || searching
@ -51,8 +53,8 @@ const PostList = ({
setSearching(true) setSearching(true)
async function fetchPosts() { async function fetchPosts() {
const res = await fetch( const res = await fetchWithUser(
`/api/post/search?q=${encodeURIComponent(query)}&userId=${userId}`, `/api/post/search?q=${encodeURIComponent(query)}`,
{ {
method: "GET", method: "GET",
headers: { headers: {
@ -61,6 +63,7 @@ const PostList = ({
} }
) )
const json = await res.json() const json = await res.json()
console.log(json)
setPosts(json) setPosts(json)
setSearching(false) setSearching(false)
} }
@ -79,22 +82,22 @@ const PostList = ({
const deletePost = useCallback( const deletePost = useCallback(
(postId: string) => async () => { (postId: string) => async () => {
const res = await fetch(`/api/post/${postId}`, { const res = await fetchWithUser(`/api/post/${postId}`, {
method: "DELETE" method: "DELETE"
}) })
if (!res.ok) { if (!res?.ok) {
console.error(res) console.error(res)
return return
} else { } else {
setPosts((posts) => posts.filter((post) => post.id !== postId)) setPosts((posts) => posts?.filter((post) => post.id !== postId))
setToast({ setToast({
message: "Post deleted", message: "Post deleted",
type: "success" type: "success"
}) })
} }
}, },
[setToast] [setPosts, setToast]
) )
return ( return (
@ -118,7 +121,7 @@ const PostList = ({
<ListItemSkeleton /> <ListItemSkeleton />
</ul> </ul>
)} )}
{!showSkeleton && posts?.length > 0 && ( {!showSkeleton && posts && posts.length > 0 ? (
<div> <div>
<ul> <ul>
{posts.map((post) => { {posts.map((post) => {
@ -134,7 +137,7 @@ const PostList = ({
})} })}
</ul> </ul>
</div> </div>
)} ) : null}
</div> </div>
) )
} }

View file

@ -66,7 +66,7 @@ const ListItem = ({
} }
return ( return (
<FadeIn as="li" key={post.id}> <FadeIn key={post.id}>
<li> <li>
<Card style={{ overflowY: "scroll" }}> <Card style={{ overflowY: "scroll" }}>
<> <>

View file

@ -16,17 +16,7 @@ const TOKENS_ENDPOINT = "/api/user/tokens"
export function useApiTokens({ userId, initialTokens }: UseApiTokens) { export function useApiTokens({ userId, initialTokens }: UseApiTokens) {
const { data, mutate, error, isLoading } = useSWR<SerializedApiToken[]>( const { data, mutate, error, isLoading } = useSWR<SerializedApiToken[]>(
"/api/user/tokens?userId=" + userId, userId ? "/api/user/tokens?userId=" + userId : null,
async (url: string) => {
return fetch(url).then(async (res) => {
const data = await res.json()
if (data.error) {
throw new Error(data.error)
}
return data
})
},
{ {
refreshInterval: 10000, refreshInterval: 10000,
fallbackData: initialTokens fallbackData: initialTokens

View file

@ -13,7 +13,8 @@ interface RootLayoutProps {
export default async function RootLayout({ children }: RootLayoutProps) { export default async function RootLayout({ children }: RootLayoutProps) {
return ( return (
<html lang="en" className={inter.variable}> // suppressHydrationWarning is required because of next-themes
<html lang="en" className={inter.variable} suppressHydrationWarning>
<head /> <head />
<body> <body>
<Toasts /> <Toasts />

View 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)
}

View file

@ -6,7 +6,7 @@ import { getAllPosts, Post } from "@lib/server/prisma"
import PostList, { NoPostsFound } from "@components/post-list" import PostList, { NoPostsFound } from "@components/post-list"
import { Suspense } from "react" import { Suspense } from "react"
const getWelcomeData = async () => { export async function getWelcomeData() {
const welcomeContent = await getWelcomeContent() const welcomeContent = await getWelcomeContent()
return welcomeContent return welcomeContent
} }

View file

@ -3,15 +3,29 @@
import * as RadixTooltip from "@radix-ui/react-tooltip" import * as RadixTooltip from "@radix-ui/react-tooltip"
import { SessionProvider } from "next-auth/react" import { SessionProvider } from "next-auth/react"
import { ThemeProvider } from "next-themes" import { ThemeProvider } from "next-themes"
import { SWRConfig } from "swr"
export function Providers({ children }: { children: React.ReactNode }) { export function Providers({ children }: { children: React.ReactNode }) {
return ( return (
<SessionProvider> <SessionProvider>
<RadixTooltip.Provider delayDuration={200}> <SWRConfig
<ThemeProvider enableSystem defaultTheme="dark"> value={{
{children} fetcher: async (url: string) => {
</ThemeProvider> const data = await fetch(url).then((res) => res.json())
</RadixTooltip.Provider> 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> </SessionProvider>
) )
} }

View file

@ -10,9 +10,9 @@ import {
useApiTokens useApiTokens
} from "src/app/hooks/swr/use-api-tokens" } from "src/app/hooks/swr/use-api-tokens"
import { copyToClipboard } from "src/app/lib/copy-to-clipboard" import { copyToClipboard } from "src/app/lib/copy-to-clipboard"
import { useSession } from "next-auth/react"
import { useState } from "react" import { useState } from "react"
import styles from "./api-keys.module.css" import styles from "./api-keys.module.css"
import { useSessionSWR } from "@lib/use-session-swr"
// need to pass in the accessToken // need to pass in the accessToken
const APIKeys = ({ const APIKeys = ({
@ -20,10 +20,10 @@ const APIKeys = ({
}: { }: {
tokens?: SerializedApiToken[] tokens?: SerializedApiToken[]
}) => { }) => {
const session = useSession() const { session } = useSessionSWR()
const { setToast } = useToasts() const { setToast } = useToasts()
const { data, error, createToken, expireToken } = useApiTokens({ const { data, error, createToken, expireToken } = useApiTokens({
userId: session.data?.user.id, userId: session?.user?.id,
initialTokens initialTokens
}) })

View file

@ -4,21 +4,27 @@ import Button from "@components/button"
import Input from "@components/input" import Input from "@components/input"
import Note from "@components/note" import Note from "@components/note"
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { useSession } from "next-auth/react" import { useSessionSWR } from "@lib/use-session-swr"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
import styles from "./profile.module.css" import styles from "./profile.module.css"
import useSWR from "swr"
import { User } from "@prisma/client"
const Profile = () => { function Profile() {
const { data: session } = useSession() const { session } = useSessionSWR()
const [name, setName] = useState<string>(session?.user.name || "") 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 [submitting, setSubmitting] = useState<boolean>(false)
const { setToast } = useToasts() const { setToast } = useToasts()
useEffect(() => { useEffect(() => {
if (!name) { if (!name && userData?.displayName) {
setName(session?.user.name || "") setName(userData?.displayName)
} }
}, [name, session]) }, [name, userData?.displayName])
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setName(e.target.value) setName(e.target.value)
@ -39,7 +45,7 @@ const Profile = () => {
displayName: name displayName: name
} }
const res = await fetch(`/api/user/${session?.user.id}`, { const res = await fetch(`/api/user/${session?.user?.id}`, {
method: "PUT", method: "PUT",
headers: { headers: {
"Content-Type": "application/json" "Content-Type": "application/json"
@ -64,7 +70,6 @@ const Profile = () => {
/* if we have their email, they signed in with OAuth */ /* if we have their email, they signed in with OAuth */
// const imageViaOauth = Boolean(session?.user.email) // const imageViaOauth = Boolean(session?.user.email)
// const TooltipComponent = ({ children }: { children: React.ReactNode }) => // const TooltipComponent = ({ children }: { children: React.ReactNode }) =>
// imageViaOauth ? ( // imageViaOauth ? (
// <Tooltip content="Change your profile image on your OAuth provider"> // <Tooltip content="Change your profile image on your OAuth provider">
@ -73,7 +78,6 @@ const Profile = () => {
// ) : ( // ) : (
// <>{children}</> // <>{children}</>
// ) // )
return ( return (
<> <>
<Note type="warning"> <Note type="warning">
@ -106,39 +110,39 @@ const Profile = () => {
/> />
</div> </div>
{/* <div> {/* <div>
<label htmlFor="image">User Avatar</label> <label htmlFor="image">User Avatar</label>
{user.image ? ( {user.image ? (
<Input <Input
id="image" id="image"
type="file" type="file"
width={"100%"} width={"100%"}
placeholder="my image" placeholder="my image"
disabled disabled
aria-label="Image" aria-label="Image"
src={user.image} src={user.image}
/> />
) : ( ) : (
<UserIcon /> <UserIcon />
)} )}
<TooltipComponent> <TooltipComponent>
<div className={styles.upload}> <div className={styles.upload}>
<input <input
type="file" type="file"
disabled={imageViaOauth} disabled={imageViaOauth}
className={styles.uploadInput} className={styles.uploadInput}
/> />
<Button <Button
type="button" type="button"
disabled={imageViaOauth} disabled={imageViaOauth}
width="100%" width="100%"
className={styles.uploadButton} className={styles.uploadButton}
aria-hidden="true" aria-hidden="true"
> >
Upload Upload
</Button> </Button>
</div> </div>
</TooltipComponent> </TooltipComponent>
</div> */} </div> */}
<Button type="submit" loading={submitting}> <Button type="submit" loading={submitting}>
Submit Submit

View file

@ -1,12 +1,19 @@
import type { GetServerSidePropsContext } from "next"
import { unstable_getServerSession } from "next-auth/next" import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./auth" import { authOptions } from "./auth"
export async function getSession() { type Params = {
return await unstable_getServerSession(authOptions) req: GetServerSidePropsContext["req"]
res: GetServerSidePropsContext["res"]
} }
export async function getCurrentUser() { export async function getSession(params?: Params) {
const session = await getSession() 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 return session?.user
} }

View file

@ -1,8 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { unstable_getServerSession } from "next-auth"
import { authOptions } from "./auth"
import { parseQueryParam } from "./parse-query-param" import { parseQueryParam } from "./parse-query-param"
import { prisma } from "./prisma" import { prisma } from "./prisma"
import { getCurrentUser } from "./session"
/** /**
* verifyApiUser checks for a `userId` param. If it exists, it checks that the * verifyApiUser checks for a `userId` param. If it exists, it checks that the
@ -23,12 +22,9 @@ export const verifyApiUser = async (
return parseAndCheckAuthToken(req) return parseAndCheckAuthToken(req)
} }
const session = await unstable_getServerSession(req, res, authOptions) const user = await getCurrentUser({ req, res })
if (!session) {
return null
}
if (session.user.id !== userId) { if (user?.id !== userId) {
return null return null
} }

View 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
}
}

View file

@ -1,8 +1,8 @@
import { withMethods } from "@lib/api-middleware/with-methods" import { withMethods } from "@lib/api-middleware/with-methods"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import { getCurrentUser } from "@lib/server/session"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "src/lib/server/prisma" import { prisma } from "src/lib/server/prisma"
import { getSession } from "next-auth/react"
import { deleteUser } from "../user/[id]" import { deleteUser } from "../user/[id]"
const actions = [ const actions = [
@ -24,19 +24,8 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return return
} }
const session = await getSession({ req }) const user = await getCurrentUser({ req, res })
const id = session?.user?.id const isAdmin = user?.role === "admin"
const isAdmin = await prisma.user
.findUnique({
where: {
id
},
select: {
role: true
}
})
.then((user) => user?.role === "admin")
if (!isAdmin) { if (!isAdmin) {
return res.status(403).json({ error: "Not authorized" }) return res.status(403).json({ error: "Not authorized" })

View file

@ -1,6 +1,7 @@
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { prisma } from "src/lib/server/prisma" import { prisma } from "src/lib/server/prisma"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import { withMethods } from "@lib/api-middleware/with-methods"
const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => { const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
const { id, download } = req.query const { id, download } = req.query
@ -30,4 +31,4 @@ const getRawFile = async (req: NextApiRequest, res: NextApiResponse) => {
res.end() res.end()
} }
export default getRawFile export default withMethods(["GET"], getRawFile)

View file

@ -1,3 +1,4 @@
import { withMethods } from "@lib/api-middleware/with-methods"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
const handler = async (_: NextApiRequest, res: NextApiResponse) => { const handler = async (_: NextApiRequest, res: NextApiResponse) => {
@ -6,4 +7,4 @@ const handler = async (_: NextApiRequest, res: NextApiResponse) => {
}) })
} }
export default handler export default withMethods(["GET"], handler)

View file

@ -2,17 +2,10 @@ import { withMethods } from "@lib/api-middleware/with-methods"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import { getPostById } from "@lib/server/prisma" import { getPostById } from "@lib/server/prisma"
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next"
import { getSession } from "next-auth/react"
import { prisma } from "src/lib/server/prisma" import { prisma } from "src/lib/server/prisma"
import * as crypto from "crypto" import * as crypto from "crypto"
import { getSession } from "@lib/server/session"
const handler = async (req: NextApiRequest, res: NextApiResponse) => { import { verifyApiUser } from "@lib/server/verify-api-user"
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)
async function handleGet(req: NextApiRequest, res: NextApiResponse<unknown>) { async function handleGet(req: NextApiRequest, res: NextApiResponse<unknown>) {
const id = parseQueryParam(req.query.id) 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") 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 // the user can always go directly to their own post
if (session?.user.id === post.authorId) { if (userId === post.authorId) {
return res.json({ return res.json({
post: post, post: post,
password: undefined password: undefined
@ -92,9 +85,8 @@ async function handlePut(req: NextApiRequest, res: NextApiResponse<unknown>) {
return res.status(404).json({ message: "Post not found" }) 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 isAuthor = session?.user.id === post.authorId
if (!isAuthor) { if (!isAuthor) {
return res.status(403).json({ message: "Unauthorized" }) return res.status(403).json({ message: "Unauthorized" })
@ -142,10 +134,10 @@ async function handleDelete(
return res.status(404).json({ message: "Post not found" }) 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 isAuthor = session?.user?.id === post.authorId
const isAdmin = session?.user.role === "admin" const isAdmin = session?.user?.role === "admin"
if (!isAuthor && !isAdmin) { if (!isAuthor && !isAdmin) {
return res.status(403).json({ message: "Unauthorized" }) return res.status(403).json({ message: "Unauthorized" })
@ -162,3 +154,11 @@ async function handleDelete(
res.json({ message: "Post deleted" }) 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)

View file

@ -1,36 +1,19 @@
import { withMethods } from "@lib/api-middleware/with-methods" import { withMethods } from "@lib/api-middleware/with-methods"
import { authOptions } from "@lib/server/auth"
import { prisma } from "@lib/server/prisma" import { prisma } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { unstable_getServerSession } from "next-auth/next"
import { File } from "@lib/server/prisma" import { File } from "@lib/server/prisma"
import * as crypto from "crypto" import * as crypto from "crypto"
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file" import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
import { verifyApiUser } from "@lib/server/verify-api-user"
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return await handlePost(req, res)
}
export default withMethods(["POST"], handler)
async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) { async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
try { try {
const session = await unstable_getServerSession(req, res, authOptions) const userId = await verifyApiUser(req, res)
if (!session || !session.user.id) { if (!userId) {
return res.status(401).json({ error: "Unauthorized" }) 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"> & { const files = req.body.files as (Omit<File, "content" | "html"> & {
content: string content: string
html: string html: string
@ -71,7 +54,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
password: hashedPassword, password: hashedPassword,
expiresAt: req.body.expiresAt, expiresAt: req.body.expiresAt,
parentId: req.body.parentId, parentId: req.body.parentId,
authorId: session.user.id, authorId: userId,
files: { files: {
create: files.map((file) => { create: files.map((file) => {
return { return {
@ -86,7 +69,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse<unknown>) {
fileHtml[files.indexOf(file)] as string, fileHtml[files.indexOf(file)] as string,
"utf-8" "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) return res.status(500).json(error)
} }
} }
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
return await handlePost(req, res)
}
export default withMethods(["POST"], handler)

View file

@ -1,39 +1,45 @@
import { withMethods } from "@lib/api-middleware/with-methods" import { withMethods } from "@lib/api-middleware/with-methods"
import { authOptions } from "@lib/server/auth"
import { parseQueryParam } from "@lib/server/parse-query-param" 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 { NextApiRequest, NextApiResponse } from "next"
import { unstable_getServerSession } from "next-auth"
const handler = async (req: NextApiRequest, res: NextApiResponse) => { 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) console.log(
"searchQuery",
const query = parseQueryParam(q) searchQuery,
const user = parseQueryParam(userId) "publicSearch",
if (!query) { publicSearch,
"userId",
query.userId
)
if (!searchQuery) {
res.status(400).json({ error: "Invalid query" }) res.status(400).json({ error: "Invalid query" })
return return
} }
try { if (publicSearch) {
let posts: ServerPostWithFiles[] const posts = await searchPosts(searchQuery)
if (session?.user.id === user || session?.user.role === "admin") { return res.json(posts)
posts = await searchPosts(query, { } else {
userId: user const userId = await verifyApiUser(req, res)
}) if (!userId) {
} else { res.status(401).json({ error: "Unauthorized" })
posts = await searchPosts(query, { return
userId: user,
publicOnly: true
})
} }
res.status(200).json(posts) const posts = await searchPosts(searchQuery, {
} catch (err) { userId,
console.error(err) withFiles: true,
res.status(500).json({ error: "Internal server error" }) publicOnly: false
})
return res.json(posts)
} }
} }

View file

@ -1,13 +1,11 @@
// https://beta.nextjs.org/docs/data-fetching/revalidating#on-demand-revalidation // 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 config from "@lib/config"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import type { NextApiRequest, NextApiResponse } from "next" import type { NextApiRequest, NextApiResponse } from "next"
export default async function handler( async function handler(req: NextApiRequest, res: NextApiResponse) {
req: NextApiRequest,
res: NextApiResponse
) {
// TODO: create a new secret? // TODO: create a new secret?
if (req.query.secret !== config.nextauth_secret) { if (req.query.secret !== config.nextauth_secret) {
return res.status(401).json({ message: "Invalid token" }) return res.status(401).json({ message: "Invalid token" })
@ -26,3 +24,5 @@ export default async function handler(
return res.status(500).send("Error revalidating") return res.status(500).send("Error revalidating")
} }
} }
export default withMethods(["GET"], handler)

View file

@ -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 * @description Deletes a user and all of their posts, files, and accounts
* @warning This function does not perform any authorization checks * @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)

View file

@ -2,11 +2,9 @@ import { parseQueryParam } from "@lib/server/parse-query-param"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { createApiToken, prisma } from "@lib/server/prisma" import { createApiToken, prisma } from "@lib/server/prisma"
import { verifyApiUser } from "@lib/server/verify-api-user" import { verifyApiUser } from "@lib/server/verify-api-user"
import { withMethods } from "@lib/api-middleware/with-methods"
export default async function handle( async function handler(req: NextApiRequest, res: NextApiResponse) {
req: NextApiRequest,
res: NextApiResponse
) {
const userId = await verifyApiUser(req, res) const userId = await verifyApiUser(req, res)
if (!userId) { if (!userId) {
return res.status(400).json({ error: "Missing userId or auth token" }) 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() return res.status(405).end()
} }
} }
export default withMethods(["GET", "POST", "DELETE"], handler)

View file

@ -1,11 +1,12 @@
// a nextjs api handerl // a nextjs api handerl
import { withMethods } from "@lib/api-middleware/with-methods"
import config from "@lib/config" import config from "@lib/config"
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file" import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
export const getWelcomeContent = async () => { export async function getWelcomeContent() {
const introContent = config.welcome_content const introContent = config.welcome_content
const introTitle = config.welcome_title 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() const welcomeContent = await getWelcomeContent()
if (!welcomeContent) { if (!welcomeContent) {
return res.status(500).json({ error: "Missing welcome content" }) 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) return res.json(welcomeContent)
} }
export default withMethods(["GET"], handler)