refactor to SWR and verifyApiUser; personal post search is broken
This commit is contained in:
parent
6fb81d77b9
commit
ba732dcd71
41 changed files with 379 additions and 328 deletions
|
@ -1,14 +1,9 @@
|
||||||
{
|
{
|
||||||
"extends": [
|
"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"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
}}
|
}} />
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" }}>
|
||||||
<>
|
<>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 />
|
||||||
|
|
12
src/app/lib/fetch-with-user.ts
Normal file
12
src/app/lib/fetch-with-user.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import { getSession } from "next-auth/react"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* a fetch wrapper that adds `userId={userId}` to the query string
|
||||||
|
*/
|
||||||
|
export async function fetchWithUser(url: string, options: RequestInit = {}) {
|
||||||
|
// TODO: figure out if this extra network call hurts performance
|
||||||
|
const session = await getSession()
|
||||||
|
const newUrl = new URL(url, process.env.NEXT_PUBLIC_DRIFT_URL)
|
||||||
|
newUrl.searchParams.append("userId", session?.user.id || "")
|
||||||
|
return fetch(newUrl.toString(), options)
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import { getAllPosts, Post } from "@lib/server/prisma"
|
||||||
import PostList, { NoPostsFound } from "@components/post-list"
|
import 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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
25
src/lib/use-session-swr.ts
Normal file
25
src/lib/use-session-swr.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { Session } from "next-auth"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
|
export function useSessionSWR() {
|
||||||
|
const {
|
||||||
|
data: session,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
isValidating,
|
||||||
|
mutate
|
||||||
|
} = useSWR<Session>("/api/auth/session")
|
||||||
|
|
||||||
|
return {
|
||||||
|
session,
|
||||||
|
error,
|
||||||
|
isLoading,
|
||||||
|
isValidating,
|
||||||
|
mutate,
|
||||||
|
/** undefined while loading */
|
||||||
|
isAuthenticated: session?.user?.id ? true : isLoading ? undefined : false,
|
||||||
|
/** undefined while loading */
|
||||||
|
isAdmin:
|
||||||
|
session?.user?.id === "admin" ? true : isLoading ? undefined : false
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,8 @@
|
||||||
import { withMethods } from "@lib/api-middleware/with-methods"
|
import { 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" })
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
Loading…
Reference in a new issue