From 9b9c3c1d875969a828c562dd60b5389ad3dd8a9a Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 9 Nov 2022 18:38:05 -0800 Subject: [PATCH] start api transition, prisma additions --- client/.env.local | 2 - client/app/(auth)/head.tsx | 5 + client/app/(auth)/signin/page.tsx | 5 + client/app/(auth)/signup/page.tsx | 12 + client/app/(posts)/new/from/[id]/page.tsx | 67 +++ client/app/(posts)/new/head.tsx | 5 + client/app/(posts)/new/page.tsx | 10 + client/app/layout.tsx | 7 +- client/app/page-wrapper.tsx | 21 + client/app/page.tsx | 17 +- client/app/prisma.ts | 52 +++ client/app/root-layout-wrapper.tsx | 66 +++ .../admin/action-dropdown/index.tsx | 2 +- client/components/admin/post-table.tsx | 2 +- client/components/admin/user-table.tsx | 2 +- client/components/app/index.tsx | 2 +- client/components/auth/index.tsx | 39 +- .../badges/created-ago-badge/index.tsx | 2 +- .../badges/expiration-badge/index.tsx | 2 +- .../badges/visibility-badge/index.tsx | 2 +- .../badges/visibility-control/index.tsx | 2 +- .../edit-document/formatting-icons/index.tsx | 2 +- client/components/edit-document/index.tsx | 2 +- client/components/error/index.tsx | 2 +- client/components/file-dropdown/index.tsx | 2 +- client/components/header/controls.tsx | 2 +- client/components/header/index.tsx | 35 +- client/components/home/index.tsx | 2 +- client/components/link/index.tsx | 11 +- .../components/new-post/description/index.tsx | 6 +- .../new-post/drag-and-drop/index.tsx | 2 +- client/components/new-post/index.tsx | 87 ++-- .../new-post/password-modal/index.tsx | 2 +- client/components/new-post/title/index.tsx | 4 +- client/components/note/note.module.css | 2 +- client/components/page-seo/index.tsx | 7 +- client/components/post-list/index.tsx | 2 +- .../post-list/list-item-skeleton.tsx | 2 +- client/components/post-list/list-item.tsx | 2 +- client/components/post-page/index.tsx | 2 +- .../post-page/password-modal-wrapper.tsx | 2 +- client/components/preview/index.tsx | 17 +- client/components/scroll-to-top/index.tsx | 2 +- client/components/settings-group/index.tsx | 2 +- .../components/settings/sections/password.tsx | 2 +- .../components/settings/sections/profile.tsx | 2 +- client/components/view-document/index.tsx | 2 +- client/lib/api/generate-access-token.ts | 27 ++ client/lib/api/get-html-from-drift-file.ts | 39 ++ client/lib/api/jwt.ts | 64 +++ client/lib/api/parse-url-query.ts | 13 + client/lib/config.ts | 89 ++++ client/lib/get-title-for-post-copy.ts | 4 +- client/lib/hooks/use-user-data.ts | 4 +- client/lib/render-markdown.tsx | 189 +++++++++ client/middleware.ts | 2 +- client/package.json | 22 +- client/pages/_document.tsx | 2 +- client/pages/admin.tsx | 2 +- client/pages/api/auth/backup.ts | 32 ++ client/pages/api/auth/requires-passcode.ts | 35 ++ client/pages/api/auth/signin.ts | 36 ++ client/pages/api/auth/signup.ts | 45 ++ client/pages/api/file/get-html.ts | 52 +++ client/pages/api/{ => file}/html/[id].ts | 0 client/pages/api/user/[slug].ts | 32 ++ client/pages/api/welcome.ts | 34 ++ client/pages/expired.tsx | 2 +- client/pages/index.backup.tsx | 2 +- client/pages/mine.tsx | 2 +- .../pages/{new => new-backup}/from/[id].tsx | 2 +- client/pages/{new => new-backup}/index.tsx | 2 +- client/pages/settings.tsx | 2 +- .../pages/{signin.tsx => signin.backup.tsx} | 2 +- .../pages/{signup.tsx => signup.backup.tsx} | 2 +- client/pnpm-lock.yaml | 389 +++++++++++++++++- .../20221110002714_init/migration.sql | 73 ++++ .../migration.sql | 19 + .../migration.sql | 28 ++ client/prisma/migrations/migration_lock.toml | 3 + client/prisma/schema.prisma | 93 +++++ .../css => styles}/react-datepicker.css | 0 82 files changed, 1671 insertions(+), 202 deletions(-) delete mode 100644 client/.env.local create mode 100644 client/app/(auth)/head.tsx create mode 100644 client/app/(auth)/signin/page.tsx create mode 100644 client/app/(auth)/signup/page.tsx create mode 100644 client/app/(posts)/new/from/[id]/page.tsx create mode 100644 client/app/(posts)/new/head.tsx create mode 100644 client/app/(posts)/new/page.tsx create mode 100644 client/app/page-wrapper.tsx create mode 100644 client/app/prisma.ts create mode 100644 client/app/root-layout-wrapper.tsx create mode 100644 client/lib/api/generate-access-token.ts create mode 100644 client/lib/api/get-html-from-drift-file.ts create mode 100644 client/lib/api/jwt.ts create mode 100644 client/lib/api/parse-url-query.ts create mode 100644 client/lib/config.ts create mode 100644 client/lib/render-markdown.tsx create mode 100644 client/pages/api/auth/backup.ts create mode 100644 client/pages/api/auth/requires-passcode.ts create mode 100644 client/pages/api/auth/signin.ts create mode 100644 client/pages/api/auth/signup.ts create mode 100644 client/pages/api/file/get-html.ts rename client/pages/api/{ => file}/html/[id].ts (100%) create mode 100644 client/pages/api/user/[slug].ts create mode 100644 client/pages/api/welcome.ts rename client/pages/{new => new-backup}/from/[id].tsx (97%) rename client/pages/{new => new-backup}/index.tsx (93%) rename client/pages/{signin.tsx => signin.backup.tsx} (88%) rename client/pages/{signup.tsx => signup.backup.tsx} (88%) create mode 100644 client/prisma/migrations/20221110002714_init/migration.sql create mode 100644 client/prisma/migrations/20221110003646_auth_token_type_fixes/migration.sql create mode 100644 client/prisma/migrations/20221110014822_file_optional_fixes/migration.sql create mode 100644 client/prisma/migrations/migration_lock.toml create mode 100644 client/prisma/schema.prisma rename client/{public/css => styles}/react-datepicker.css (100%) diff --git a/client/.env.local b/client/.env.local deleted file mode 100644 index 1eac9b7c..00000000 --- a/client/.env.local +++ /dev/null @@ -1,2 +0,0 @@ -API_URL=http://localhost:3000 -SECRET_KEY=secret diff --git a/client/app/(auth)/head.tsx b/client/app/(auth)/head.tsx new file mode 100644 index 00000000..cea829b8 --- /dev/null +++ b/client/app/(auth)/head.tsx @@ -0,0 +1,5 @@ +import PageSeo from "@components/head" + +export default function AuthHead() { + return +} diff --git a/client/app/(auth)/signin/page.tsx b/client/app/(auth)/signin/page.tsx new file mode 100644 index 00000000..1e9469f5 --- /dev/null +++ b/client/app/(auth)/signin/page.tsx @@ -0,0 +1,5 @@ +import Auth from "@components/auth" + +export default function SignInPage() { + return +} diff --git a/client/app/(auth)/signup/page.tsx b/client/app/(auth)/signup/page.tsx new file mode 100644 index 00000000..af8f5e8c --- /dev/null +++ b/client/app/(auth)/signup/page.tsx @@ -0,0 +1,12 @@ +import Auth from "@components/auth" +import config from "@lib/config" +import { getRequiresPasscode } from "pages/api/auth/requires-passcode" + +const getPasscode = async () => { + return await getRequiresPasscode() +} + +export default async function SignUpPage() { + const requiresPasscode = await getPasscode() + return +} diff --git a/client/app/(posts)/new/from/[id]/page.tsx b/client/app/(posts)/new/from/[id]/page.tsx new file mode 100644 index 00000000..b20974c8 --- /dev/null +++ b/client/app/(posts)/new/from/[id]/page.tsx @@ -0,0 +1,67 @@ +import styles from "@styles/Home.module.css" +import NewPost from "@components/new-post" +import PageSeo from "@components/page-seo" +import { Page } from "@geist-ui/core/dist" +import Head from "next/head" +import { GetServerSideProps } from "next" +import { Post } from "@lib/types" +import cookie from "cookie" + + +const NewFromExisting = async () => { + return ( + // + // {/* TODO: solve this. */} + // {/* eslint-disable-next-line @next/next/no-css-tags */} + // + // + + ) +} + +// export const getServerSideProps: GetServerSideProps = async ({ +// req, +// params +// }) => { +// const id = params?.id +// const redirect = { +// redirect: { +// destination: "/new", +// permanent: false +// } +// } + +// if (!id) { +// return redirect +// } + +// const driftToken = cookie.parse(req.headers.cookie || "")[`drift-token`] + +// const post = await fetch(`${process.env.API_URL}/posts/${id}`, { +// method: "GET", +// headers: { +// "Content-Type": "application/json", +// Authorization: `Bearer ${driftToken}`, +// "x-secret-key": process.env.SECRET_KEY || "" +// } +// }) + +// if (!post.ok) { +// return redirect +// } + +// const data = await post.json() + +// if (!data) { +// return redirect +// } + +// return { +// props: { +// post: data, +// parentId: id +// } +// } +// } + +export default NewFromExisting diff --git a/client/app/(posts)/new/head.tsx b/client/app/(posts)/new/head.tsx new file mode 100644 index 00000000..8acc8e7d --- /dev/null +++ b/client/app/(posts)/new/head.tsx @@ -0,0 +1,5 @@ +import PageSeo from "@components/page-seo" + +export default function NewPostHead() { + return +} diff --git a/client/app/(posts)/new/page.tsx b/client/app/(posts)/new/page.tsx new file mode 100644 index 00000000..ed68e74f --- /dev/null +++ b/client/app/(posts)/new/page.tsx @@ -0,0 +1,10 @@ +import NewPost from "@components/new-post" +import '@styles/react-datepicker.css' + +const New = () => { + return ( + + ) +} + +export default New diff --git a/client/app/layout.tsx b/client/app/layout.tsx index 78994a13..8bc39b88 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -1,5 +1,6 @@ -import "styles/globals.css" +import "@styles/globals.css" import { ServerThemeProvider } from "next-themes" +import { LayoutWrapper } from "./root-layout-wrapper" interface RootLayoutProps { children: React.ReactNode @@ -8,9 +9,9 @@ interface RootLayoutProps { export default function RootLayout({ children }: RootLayoutProps) { return ( @@ -48,7 +49,7 @@ export default function RootLayout({ children }: RootLayoutProps) { Drift - {children} + {children} ) diff --git a/client/app/page-wrapper.tsx b/client/app/page-wrapper.tsx new file mode 100644 index 00000000..86d2f22d --- /dev/null +++ b/client/app/page-wrapper.tsx @@ -0,0 +1,21 @@ +"use client" + +import Header from "@components/header" +import { Page } from "@geist-ui/core/dist" +import styles from "@styles/Home.module.css" + +export default function PageWrapper({ + children +}: { + children: React.ReactNode +}) { + return ( + + +
+ + + {children} + + ) +} diff --git a/client/app/page.tsx b/client/app/page.tsx index e701f3f9..56c3c4d7 100644 --- a/client/app/page.tsx +++ b/client/app/page.tsx @@ -1,7 +1,12 @@ -export default function Page() { - return ( - <> -

Hello, world!

- - ) +import { getWelcomeContent } from "pages/api/welcome" + +const getWelcomeData = async () => { + const welcomeContent = await getWelcomeContent() + return welcomeContent +} + +export default async function Page() { + const welcomeData = await getWelcomeData() + + return

{JSON.stringify(welcomeData)}

} diff --git a/client/app/prisma.ts b/client/app/prisma.ts new file mode 100644 index 00000000..9ef83abe --- /dev/null +++ b/client/app/prisma.ts @@ -0,0 +1,52 @@ +import { Post, PrismaClient, File } from "@prisma/client" + +const prisma = new PrismaClient() + +export default prisma + +export type { + User, + AuthTokens, + File, + Post, + PostToAuthors +} from "@prisma/client" + +export type PostWithFiles = Post & { + files: File[] +} + +export const getFilesForPost = async (postId: string) => { + const files = await prisma.file.findMany({ + where: { + postId + } + }) + + return files +} + +export const getPostWithFiles = async ( + postId: string +): Promise => { + const post = await prisma.post.findUnique({ + where: { + id: postId + } + }) + + if (!post) { + return undefined + } + + const files = await getFilesForPost(postId) + + if (!files) { + return undefined + } + + return { + ...post, + files + } +} diff --git a/client/app/root-layout-wrapper.tsx b/client/app/root-layout-wrapper.tsx new file mode 100644 index 00000000..bd612503 --- /dev/null +++ b/client/app/root-layout-wrapper.tsx @@ -0,0 +1,66 @@ +"use client" + +import { CssBaseline, GeistProvider, Themes } from "@geist-ui/core/dist" +import { ThemeProvider } from "next-themes" +import { SkeletonTheme } from "react-loading-skeleton" +import PageWrapper from "./page-wrapper" + +export function LayoutWrapper({ children }: { children: React.ReactNode }) { + const skeletonBaseColor = "var(--light-gray)" + const skeletonHighlightColor = "var(--lighter-gray)" + + const customTheme = Themes.createFromLight({ + type: "custom", + palette: { + background: "var(--bg)", + foreground: "var(--fg)", + accents_1: "var(--lightest-gray)", + accents_2: "var(--lighter-gray)", + accents_3: "var(--light-gray)", + accents_4: "var(--gray)", + accents_5: "var(--darker-gray)", + accents_6: "var(--darker-gray)", + accents_7: "var(--darkest-gray)", + accents_8: "var(--darkest-gray)", + border: "var(--light-gray)", + warning: "var(--warning)" + }, + expressiveness: { + dropdownBoxShadow: "0 0 0 1px var(--light-gray)", + shadowSmall: "0 0 0 1px var(--light-gray)", + shadowLarge: "0 0 0 1px var(--light-gray)", + shadowMedium: "0 0 0 1px var(--light-gray)" + }, + layout: { + gap: "var(--gap)", + gapHalf: "var(--gap-half)", + gapQuarter: "var(--gap-quarter)", + gapNegative: "var(--gap-negative)", + gapHalfNegative: "var(--gap-half-negative)", + gapQuarterNegative: "var(--gap-quarter-negative)", + radius: "var(--radius)" + }, + font: { + mono: "var(--font-mono)", + sans: "var(--font-sans)" + } + }) + + return ( + + + + + {children} + + + + ) +} diff --git a/client/components/admin/action-dropdown/index.tsx b/client/components/admin/action-dropdown/index.tsx index 41516896..11256170 100644 --- a/client/components/admin/action-dropdown/index.tsx +++ b/client/components/admin/action-dropdown/index.tsx @@ -1,4 +1,4 @@ -import { Popover, Button } from "@geist-ui/core" +import { Popover, Button } from "@geist-ui/core/dist" import { MoreVertical } from "@geist-ui/icons" type Action = { diff --git a/client/components/admin/post-table.tsx b/client/components/admin/post-table.tsx index 6bbb52d1..312575bf 100644 --- a/client/components/admin/post-table.tsx +++ b/client/components/admin/post-table.tsx @@ -1,5 +1,5 @@ import SettingsGroup from "@components/settings-group" -import { Fieldset, useToasts } from "@geist-ui/core" +import { Fieldset, useToasts } from "@geist-ui/core/dist" import byteToMB from "@lib/byte-to-mb" import { Post } from "@lib/types" import Table from "rc-table" diff --git a/client/components/admin/user-table.tsx b/client/components/admin/user-table.tsx index b931db70..61ae8d4b 100644 --- a/client/components/admin/user-table.tsx +++ b/client/components/admin/user-table.tsx @@ -1,4 +1,4 @@ -import { Fieldset, useToasts } from "@geist-ui/core" +import { Fieldset, useToasts } from "@geist-ui/core/dist" import { User } from "@lib/types" import { useEffect, useMemo, useState } from "react" import { adminFetcher } from "." diff --git a/client/components/app/index.tsx b/client/components/app/index.tsx index ab0a85a2..943ff951 100644 --- a/client/components/app/index.tsx +++ b/client/components/app/index.tsx @@ -1,5 +1,5 @@ import Header from "@components/header" -import { GeistProvider, CssBaseline, Themes, Page } from "@geist-ui/core" +import { GeistProvider, CssBaseline, Themes, Page } from "@geist-ui/core/dist" import type { NextComponentType, NextPageContext } from "next" import { SkeletonTheme } from "react-loading-skeleton" diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index a5e48fe7..f025c9ef 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -1,6 +1,8 @@ +"use client" + import { FormEvent, useEffect, useState } from "react" import styles from "./auth.module.css" -import { useRouter } from "next/router" +import { useRouter } from "next/navigation" import Link from "../link" import Cookies from "js-cookie" import useSignedIn from "@lib/hooks/use-signed-in" @@ -12,32 +14,21 @@ const NO_EMPTY_SPACE_REGEX = /^\S*$/ const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters" -const Auth = ({ page }: { page: "signup" | "signin" }) => { +const Auth = ({ + page, + requiresServerPassword +}: { + page: "signup" | "signin" + requiresServerPassword?: boolean +}) => { const router = useRouter() const [username, setUsername] = useState("") const [password, setPassword] = useState("") const [serverPassword, setServerPassword] = useState("") const [errorMsg, setErrorMsg] = useState("") - const [requiresServerPassword, setRequiresServerPassword] = useState(false) const signingIn = page === "signin" const { signin } = useSignedIn() - useEffect(() => { - async function fetchRequiresPass() { - if (!signingIn) { - const resp = await fetch("/server-api/auth/requires-passcode", { - method: "GET" - }) - if (resp.ok) { - const res = await resp.json() - setRequiresServerPassword(res.requiresPasscode) - } else { - setErrorMsg("Something went wrong. Is the server running?") - } - } - } - fetchRequiresPass() - }, [page, signingIn]) const handleJson = (json: any) => { signin(json.token) @@ -70,9 +61,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => { } try { - const signUrl = signingIn - ? "/server-api/auth/signin" - : "/server-api/auth/signup" + const signUrl = signingIn ? "/api/auth/signin" : "/api/auth/signup" const resp = await fetch(signUrl, reqOpts) const json = await resp.json() if (!resp.ok) throw new Error(json.error.message) @@ -141,11 +130,7 @@ const Auth = ({ page }: { page: "signup" | "signin" }) => {

)} - {errorMsg && ( - - {errorMsg} - - )} + {errorMsg && {errorMsg}} diff --git a/client/components/badges/created-ago-badge/index.tsx b/client/components/badges/created-ago-badge/index.tsx index 9b84991b..3680beef 100644 --- a/client/components/badges/created-ago-badge/index.tsx +++ b/client/components/badges/created-ago-badge/index.tsx @@ -1,4 +1,4 @@ -import { Badge, Tooltip } from "@geist-ui/core" +import { Badge, Tooltip } from "@geist-ui/core/dist" import { timeAgo } from "@lib/time-ago" import { useMemo, useState, useEffect } from "react" diff --git a/client/components/badges/expiration-badge/index.tsx b/client/components/badges/expiration-badge/index.tsx index 9151d915..0326f19c 100644 --- a/client/components/badges/expiration-badge/index.tsx +++ b/client/components/badges/expiration-badge/index.tsx @@ -1,4 +1,4 @@ -import { Badge, Tooltip } from "@geist-ui/core" +import { Badge, Tooltip } from "@geist-ui/core/dist" import { timeUntil } from "@lib/time-ago" import { useCallback, useEffect, useMemo, useState } from "react" diff --git a/client/components/badges/visibility-badge/index.tsx b/client/components/badges/visibility-badge/index.tsx index 0385463f..42fc899f 100644 --- a/client/components/badges/visibility-badge/index.tsx +++ b/client/components/badges/visibility-badge/index.tsx @@ -1,4 +1,4 @@ -import { Badge } from "@geist-ui/core" +import { Badge } from "@geist-ui/core/dist" import type { PostVisibility } from "@lib/types" type Props = { diff --git a/client/components/badges/visibility-control/index.tsx b/client/components/badges/visibility-control/index.tsx index 2fbcd9dc..72234e9d 100644 --- a/client/components/badges/visibility-control/index.tsx +++ b/client/components/badges/visibility-control/index.tsx @@ -1,5 +1,5 @@ import PasswordModal from "@components/new-post/password-modal" -import { Button, ButtonGroup, Loading, useToasts } from "@geist-ui/core" +import { Button, ButtonGroup, Loading, useToasts } from "@geist-ui/core/dist" import type { PostVisibility } from "@lib/types" import Cookies from "js-cookie" import { useCallback, useState } from "react" diff --git a/client/components/edit-document/formatting-icons/index.tsx b/client/components/edit-document/formatting-icons/index.tsx index b3b0a06d..26631b1b 100644 --- a/client/components/edit-document/formatting-icons/index.tsx +++ b/client/components/edit-document/formatting-icons/index.tsx @@ -7,7 +7,7 @@ import List from "@geist-ui/icons/list" import ImageIcon from "@geist-ui/icons/image" import { RefObject, useMemo } from "react" import styles from "../document.module.css" -import { Button, ButtonGroup, Tooltip } from "@geist-ui/core" +import { Button, ButtonGroup, Tooltip } from "@geist-ui/core/dist" import { TextareaMarkdownRef } from "textarea-markdown-editor" // TODO: clean up diff --git a/client/components/edit-document/index.tsx b/client/components/edit-document/index.tsx index a675f17a..cdb29d69 100644 --- a/client/components/edit-document/index.tsx +++ b/client/components/edit-document/index.tsx @@ -11,7 +11,7 @@ import Trash from "@geist-ui/icons/trash" import FormattingIcons from "./formatting-icons" import TextareaMarkdown, { TextareaMarkdownRef } from "textarea-markdown-editor" -import { Button, Input, Spacer, Tabs, Textarea } from "@geist-ui/core" +import { Button, Input, Spacer, Tabs, Textarea } from "@geist-ui/core/dist" import Preview from "@components/preview" // import Link from "next/link" diff --git a/client/components/error/index.tsx b/client/components/error/index.tsx index 1b262243..a2b7b72e 100644 --- a/client/components/error/index.tsx +++ b/client/components/error/index.tsx @@ -1,4 +1,4 @@ -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" const Error = ({ status }: { status: number }) => { return ( diff --git a/client/components/file-dropdown/index.tsx b/client/components/file-dropdown/index.tsx index b9ef188e..e299b3e1 100644 --- a/client/components/file-dropdown/index.tsx +++ b/client/components/file-dropdown/index.tsx @@ -1,5 +1,5 @@ import ShiftBy from "@components/shift-by" -import { Button, Popover } from "@geist-ui/core" +import { Button, Popover } from "@geist-ui/core/dist" import ChevronDown from "@geist-ui/icons/chevronDown" import CodeIcon from "@geist-ui/icons/fileFunction" import FileIcon from "@geist-ui/icons/fileText" diff --git a/client/components/header/controls.tsx b/client/components/header/controls.tsx index 49b7ff45..ba949952 100644 --- a/client/components/header/controls.tsx +++ b/client/components/header/controls.tsx @@ -3,7 +3,7 @@ import MoonIcon from "@geist-ui/icons/moon" import SunIcon from "@geist-ui/icons/sun" // import { useAllThemes, useTheme } from '@geist-ui/core' import styles from "./header.module.css" -import { Select } from "@geist-ui/core" +import { Select } from "@geist-ui/core/dist" import { useTheme } from "next-themes" const Controls = () => { diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx index 869d12bd..a53b954b 100644 --- a/client/components/header/index.tsx +++ b/client/components/header/index.tsx @@ -1,4 +1,4 @@ -'use client'; +"use client" import { ButtonGroup, @@ -7,7 +7,7 @@ import { Spacer, useBodyScroll, useMediaQuery -} from "@geist-ui/core" +} from "@geist-ui/core/dist" import { useCallback, useEffect, useMemo, useState } from "react" import styles from "./header.module.css" @@ -27,7 +27,7 @@ import SunIcon from "@geist-ui/icons/sun" import { useTheme } from "next-themes" import useUserData from "@lib/hooks/use-user-data" import Link from "next/link" -import { useRouter } from "next/router" +import { usePathname } from "next/navigation" type Tab = { name: string @@ -38,7 +38,7 @@ type Tab = { } const Header = () => { - const router = useRouter() + const pathname = usePathname() const [expanded, setExpanded] = useState(false) const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true }) const isMobile = useMediaQuery("xs", { match: "down" }) @@ -154,7 +154,7 @@ const Header = () => { const getButton = useCallback( (tab: Tab) => { - const activeStyle = router.pathname === tab.href ? styles.active : "" + const activeStyle = pathname === tab.href ? styles.active : "" if (tab.onClick) { return ( - - ) - ); + + + + ) } }, - [isMobile, onTabChange, router.pathname] + [isMobile, onTabChange, pathname] ) const buttons = useMemo(() => pages.map(getButton), [pages, getButton]) diff --git a/client/components/home/index.tsx b/client/components/home/index.tsx index 557a2f5e..332f8839 100644 --- a/client/components/home/index.tsx +++ b/client/components/home/index.tsx @@ -1,5 +1,5 @@ import ShiftBy from "@components/shift-by" -import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core" +import { Spacer, Tabs, Card, Textarea, Text } from "@geist-ui/core/dist" import Image from "next/image" import styles from "./home.module.css" import markdownStyles from "@components/preview/preview.module.css" diff --git a/client/components/link/index.tsx b/client/components/link/index.tsx index 6a99a1e5..e130f579 100644 --- a/client/components/link/index.tsx +++ b/client/components/link/index.tsx @@ -3,21 +3,14 @@ import NextLink from "next/link" import styles from "./link.module.css" type LinkProps = { - href: string, colored?: boolean, children: React.ReactNode } & React.ComponentProps -const Link = ({ href, colored, children, ...props }: LinkProps) => { - const { basePath } = useRouter() - const propHrefWithoutLeadingSlash = - href && href.startsWith("/") ? href.substring(1) : href - - const url = basePath ? `${basePath}/${propHrefWithoutLeadingSlash}` : href - +const Link = ({ colored, children, ...props }: LinkProps) => { const className = colored ? `${styles.link} ${styles.color}` : styles.link return ( - + {children} ) diff --git a/client/components/new-post/description/index.tsx b/client/components/new-post/description/index.tsx index e4a05a11..b5bde981 100644 --- a/client/components/new-post/description/index.tsx +++ b/client/components/new-post/description/index.tsx @@ -1,18 +1,18 @@ import { ChangeEvent, memo } from "react" -import { Input } from "@geist-ui/core" +import { Input } from "@geist-ui/core/dist" import styles from "../post.module.css" type props = { onChange: (e: ChangeEvent) => void - description?: string + description: string } const Description = ({ onChange, description }: props) => { return (
{ const { setToast } = useToasts() const router = useRouter() - const [title, setTitle] = useState() - const [description, setDescription] = useState() - const [expiresAt, setExpiresAt] = useState(null) - - const emptyDoc = useMemo( - () => [ - { - title: "", - content: "", - id: generateUUID() - } - ], - [] + const [title, setTitle] = useState( + getTitleForPostCopy(initialPost?.title) || "" ) + const [description, setDescription] = useState(initialPost?.description || "") + const [expiresAt, setExpiresAt] = useState(initialPost?.expiresAt) - const [docs, setDocs] = useState(emptyDoc) + const defaultDocs: DocumentType[] = initialPost + ? initialPost.files?.map((doc) => ({ + title: doc.title, + content: doc.content, + id: doc.id + })) + : [emptyDoc] - // the /new/from/{id} route fetches an initial post - useEffect(() => { - if (initialPost) { - setDocs( - initialPost.files?.map((doc) => ({ - title: doc.title, - content: doc.content, - id: doc.id - })) || emptyDoc - ) - - setTitle(getTitleForPostCopy(initialPost.title)) - setDescription(initialPost.description) - } - }, [emptyDoc, initialPost]) + const [docs, setDocs] = useState(defaultDocs) const [passwordModalVisible, setPasswordModalVisible] = useState(false) @@ -151,13 +139,13 @@ const Post = ({ return } - await sendRequest("/server-api/posts/create", { + await sendRequest("/api/posts/create", { title, files: docs, visibility, password, userId: Cookies.get("drift-userid") || "", - expiresAt, + expiresAt: expiresAt || null, parentId: newPostParent }) }, @@ -233,18 +221,15 @@ const Post = ({ // })) // } - const onPaste = useCallback( - (e: any) => { - const pastedText = e.clipboardData.getData("text") + const onPaste = (e: ClipboardEvent) => { + const pastedText = e.clipboardData?.getData("text") - if (pastedText) { - if (!title) { - setTitle("Pasted text") - } + if (pastedText) { + if (!title) { + setTitle("Pasted text") } - }, - [title] - ) + } + } const CustomTimeInput = ({ date, @@ -328,10 +313,10 @@ const Post = ({ /> } - onSubmit("unlisted")}> + onSubmit("unlisted")}> Create Unlisted - onSubmit("private")}> + onSubmit("private")}> Create Private onSubmit("public")}> diff --git a/client/components/new-post/password-modal/index.tsx b/client/components/new-post/password-modal/index.tsx index 379b09cc..c15aae6e 100644 --- a/client/components/new-post/password-modal/index.tsx +++ b/client/components/new-post/password-modal/index.tsx @@ -1,4 +1,4 @@ -import { Modal, Note, Spacer, Input } from "@geist-ui/core" +import { Modal, Note, Spacer, Input } from "@geist-ui/core/dist" import { useState } from "react" type Props = { diff --git a/client/components/new-post/title/index.tsx b/client/components/new-post/title/index.tsx index ca79896e..7e781081 100644 --- a/client/components/new-post/title/index.tsx +++ b/client/components/new-post/title/index.tsx @@ -1,9 +1,9 @@ import { ChangeEvent, memo, useEffect, useState } from "react" -import { Text } from "@geist-ui/core" +import { Text } from "@geist-ui/core/dist" import ShiftBy from "@components/shift-by" import styles from "../post.module.css" -import { Input } from "@geist-ui/core" +import { Input } from "@geist-ui/core/dist" const titlePlaceholders = [ "How to...", diff --git a/client/components/note/note.module.css b/client/components/note/note.module.css index c1b2499f..0928af3c 100644 --- a/client/components/note/note.module.css +++ b/client/components/note/note.module.css @@ -17,7 +17,7 @@ } .error { - background: red; + background: #f33; } .type { diff --git a/client/components/page-seo/index.tsx b/client/components/page-seo/index.tsx index a586544f..4083f2c4 100644 --- a/client/components/page-seo/index.tsx +++ b/client/components/page-seo/index.tsx @@ -1,4 +1,3 @@ -import Head from "next/head" import React from "react" type PageSeoProps = { @@ -15,10 +14,8 @@ const PageSeo = ({ }: PageSeoProps) => { return ( <> - - {title} - {!isPrivate && } - + Drift - {title} + {!isPrivate && } ) } diff --git a/client/components/post-list/index.tsx b/client/components/post-list/index.tsx index 6db19f07..67d890a1 100644 --- a/client/components/post-list/index.tsx +++ b/client/components/post-list/index.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Text } from "@geist-ui/core" +import { Button, Input, Text } from "@geist-ui/core/dist" import styles from "./post-list.module.css" import ListItemSkeleton from "./list-item-skeleton" diff --git a/client/components/post-list/list-item-skeleton.tsx b/client/components/post-list/list-item-skeleton.tsx index ac8078a9..71bd1a18 100644 --- a/client/components/post-list/list-item-skeleton.tsx +++ b/client/components/post-list/list-item-skeleton.tsx @@ -1,5 +1,5 @@ import Skeleton from "react-loading-skeleton" -import { Card, Divider, Grid, Spacer } from "@geist-ui/core" +import { Card, Divider, Grid, Spacer } from "@geist-ui/core/dist" const ListItemSkeleton = () => ( diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx index f51604d0..a5a35e82 100644 --- a/client/components/post-list/list-item.tsx +++ b/client/components/post-list/list-item.tsx @@ -1,6 +1,6 @@ import NextLink from "next/link" import VisibilityBadge from "../badges/visibility-badge" -import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core" +import { Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core/dist" import { File, Post } from "@lib/types" import FadeIn from "@components/fade-in" import Trash from "@geist-ui/icons/trash" diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index fa9c0f55..49415040 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -5,7 +5,7 @@ import styles from "./post-page.module.css" import homeStyles from "@styles/Home.module.css" import type { File, Post, PostVisibility } from "@lib/types" -import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core" +import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core/dist" import { useEffect, useState } from "react" import Archive from "@geist-ui/icons/archive" import Edit from "@geist-ui/icons/edit" diff --git a/client/components/post-page/password-modal-wrapper.tsx b/client/components/post-page/password-modal-wrapper.tsx index 4ece7d37..cf5bf142 100644 --- a/client/components/post-page/password-modal-wrapper.tsx +++ b/client/components/post-page/password-modal-wrapper.tsx @@ -1,5 +1,5 @@ import PasswordModal from "@components/new-post/password-modal" -import { Page, useToasts } from "@geist-ui/core" +import { Page, useToasts } from "@geist-ui/core/dist" import { Post } from "@lib/types" import { useRouter } from "next/router" import { useState } from "react" diff --git a/client/components/preview/index.tsx b/client/components/preview/index.tsx index 8aaefff1..cbf7c0fa 100644 --- a/client/components/preview/index.tsx +++ b/client/components/preview/index.tsx @@ -16,7 +16,7 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => { useEffect(() => { async function fetchPost() { if (fileId) { - const resp = await fetch(`/api/html/${fileId}`, { + const resp = await fetch(`/api/file/html/${fileId}`, { method: "GET" }) if (resp.ok) { @@ -25,16 +25,19 @@ const MarkdownPreview = ({ height = 500, fileId, content, title }: Props) => { setIsLoading(false) } } else if (content) { - const resp = await fetch("/server-api/files/html", { - method: "POST", + // add title and query to url params + const urlQuery = new URLSearchParams({ + title: title || "", + content + }) + + const resp = await fetch(`/api/files/get-html?${urlQuery}`, { + method: "GET", headers: { "Content-Type": "application/json", Authorization: `Bearer ${Cookies.get("drift-token") || ""}` }, - body: JSON.stringify({ - title, - content - }) + }) if (resp.ok) { diff --git a/client/components/scroll-to-top/index.tsx b/client/components/scroll-to-top/index.tsx index f3db3111..3d3f55a4 100644 --- a/client/components/scroll-to-top/index.tsx +++ b/client/components/scroll-to-top/index.tsx @@ -1,4 +1,4 @@ -import { Tooltip, Button, Spacer } from "@geist-ui/core" +import { Tooltip, Button, Spacer } from "@geist-ui/core/dist" import ChevronUp from "@geist-ui/icons/chevronUpCircleFill" import { useEffect, useState } from "react" import styles from "./scroll.module.css" diff --git a/client/components/settings-group/index.tsx b/client/components/settings-group/index.tsx index 9325dce9..dde629f1 100644 --- a/client/components/settings-group/index.tsx +++ b/client/components/settings-group/index.tsx @@ -1,4 +1,4 @@ -import { Fieldset, Text, Divider } from "@geist-ui/core" +import { Fieldset, Text, Divider } from "@geist-ui/core/dist" import styles from "./settings-group.module.css" type Props = { diff --git a/client/components/settings/sections/password.tsx b/client/components/settings/sections/password.tsx index 9ef92770..19beb12f 100644 --- a/client/components/settings/sections/password.tsx +++ b/client/components/settings/sections/password.tsx @@ -1,4 +1,4 @@ -import { Input, Button, useToasts } from "@geist-ui/core" +import { Input, Button, useToasts } from "@geist-ui/core/dist" import Cookies from "js-cookie" import { useState } from "react" diff --git a/client/components/settings/sections/profile.tsx b/client/components/settings/sections/profile.tsx index 8cd7dbb0..7e36ef08 100644 --- a/client/components/settings/sections/profile.tsx +++ b/client/components/settings/sections/profile.tsx @@ -1,4 +1,4 @@ -import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core" +import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist" import useUserData from "@lib/hooks/use-user-data" import Cookies from "js-cookie" import { useEffect, useState } from "react" diff --git a/client/components/view-document/index.tsx b/client/components/view-document/index.tsx index 237ec42e..f96a99ed 100644 --- a/client/components/view-document/index.tsx +++ b/client/components/view-document/index.tsx @@ -13,7 +13,7 @@ import { Textarea, Tooltip, Tag -} from "@geist-ui/core" +} from "@geist-ui/core/dist" import HtmlPreview from "@components/preview" import FadeIn from "@components/fade-in" diff --git a/client/lib/api/generate-access-token.ts b/client/lib/api/generate-access-token.ts new file mode 100644 index 00000000..1d5aa1d9 --- /dev/null +++ b/client/lib/api/generate-access-token.ts @@ -0,0 +1,27 @@ +import config from "@lib/config" +import { User } from "@prisma/client" +import prisma from "app/prisma" +import { sign } from "jsonwebtoken" + +export async function generateAccessToken(user: User) { + const token = sign({ id: user.id }, config.jwt_secret, { expiresIn: "2d" }) + + await prisma.authTokens.create({ + data: { + userId: user.id, + token: token + } + }) + + // TODO: set expiredReason? + prisma.authTokens.deleteMany({ + where: { + userId: user.id, + token: { + not: token + } + } + }) + + return token +} diff --git a/client/lib/api/get-html-from-drift-file.ts b/client/lib/api/get-html-from-drift-file.ts new file mode 100644 index 00000000..8aaeaac9 --- /dev/null +++ b/client/lib/api/get-html-from-drift-file.ts @@ -0,0 +1,39 @@ +import markdown from "../render-markdown" +import type { File } from "app/prisma" +/** + * returns rendered HTML from a Drift file + */ +function getHtmlFromFile({ content, title }: Pick) { + const renderAsMarkdown = [ + "markdown", + "md", + "mdown", + "mkdn", + "mkd", + "mdwn", + "mdtxt", + "mdtext", + "text", + "" + ] + const fileType = () => { + const pathParts = title.split(".") + const language = pathParts.length > 1 ? pathParts[pathParts.length - 1] : "" + return language + } + const type = fileType() + let contentToRender: string = content || "" + + if (!renderAsMarkdown.includes(type)) { + contentToRender = `~~~${type} +${content} +~~~` + } else { + contentToRender = "\n" + content + } + + const html = markdown(contentToRender) + return html +} + +export default getHtmlFromFile diff --git a/client/lib/api/jwt.ts b/client/lib/api/jwt.ts new file mode 100644 index 00000000..6896e581 --- /dev/null +++ b/client/lib/api/jwt.ts @@ -0,0 +1,64 @@ +// next api route jwt middleware; check if the user has a valid jwt token + +import config from "@lib/config" +import { User } from "@prisma/client" +import prisma from "app/prisma" +import * as jwt from "jsonwebtoken" +import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next" + +type ReqWithUser = NextApiRequest & { + user?: User +} + +type WrappedHandler = (req: ReqWithUser, res: NextApiResponse) => Promise + +// usage: useJwt(otherHandler) + +// we want the usage to be the user writing their API route and exporting it with useJwt(handler) + +// uses prisma +export async function withJwt( + origHandler: NextApiHandler +): Promise { + return async (req: ReqWithUser, res: NextApiResponse) => { + const authHeader = req ? req.headers["authorization"] : undefined + const token = authHeader && authHeader.split(" ")[1] + + if (token == null) return res.status(401).send("Unauthorized") + + const authToken = await prisma.authTokens.findUnique({ + where: { id: token } + }) + if (authToken == null) { + return res.status(401).send("Unauthorized") + } + + if (authToken.deletedAt) { + return res.status(401).json({ + message: "Token is no longer valid" + }) + } + + jwt.verify(token, config.jwt_secret, async (err: any, user: any) => { + if (err) return res.status(403).send("Forbidden") + const userObj = await prisma.user.findUnique({ + where: { id: user.id }, + select: { + id: true, + email: true, + displayName: true, + bio: true, + createdAt: true, + updatedAt: true, + deletedAt: true + } + }) + if (!userObj) { + return res.status(403).send("Forbidden") + } + + ;(req as ReqWithUser).user = user + return origHandler(req, res) + }) + } +} diff --git a/client/lib/api/parse-url-query.ts b/client/lib/api/parse-url-query.ts new file mode 100644 index 00000000..b542d775 --- /dev/null +++ b/client/lib/api/parse-url-query.ts @@ -0,0 +1,13 @@ +/* + * Parses a URL query string from string | string[] | ... + * to string | undefined. If it's an array, we return the last item. + */ +export function parseUrlQuery(query: string | string[] | undefined) { + if (typeof query === "string") { + return query + } else if (Array.isArray(query)) { + return query[query.length - 1] + } else { + return undefined + } +} diff --git a/client/lib/config.ts b/client/lib/config.ts new file mode 100644 index 00000000..7059cc12 --- /dev/null +++ b/client/lib/config.ts @@ -0,0 +1,89 @@ +type Config = { + // port: number + jwt_secret: string + drift_home: string + is_production: boolean + memory_db: boolean + enable_admin: boolean + secret_key: string + registration_password: string + welcome_content: string + welcome_title: string + url: string +} + +type EnvironmentValue = string | undefined +type Environment = { [key: string]: EnvironmentValue } + +export const config = (env: Environment): Config => { + const stringToBoolean = (str: EnvironmentValue): boolean => { + if (str === "true") { + return true + } else if (str === "false") { + return false + } else if (str) { + throw new Error(`Invalid boolean value: ${str}`) + } else { + return false + } + } + + const throwIfUndefined = (str: EnvironmentValue, name: string): string => { + if (str === undefined) { + throw new Error(`Missing environment variable: ${name}`) + } + return str + } + + const defaultIfUndefined = ( + str: EnvironmentValue, + defaultValue: string + ): string => { + if (str === undefined) { + return defaultValue + } + return str + } + + const validNodeEnvs = (str: EnvironmentValue) => { + const valid = ["development", "production", "test"] + if (str && !valid.includes(str)) { + throw new Error(`Invalid NODE_ENV set: ${str}`) + } else if (!str) { + console.warn("No NODE_ENV specified, defaulting to development") + } else { + console.log(`Using NODE_ENV: ${str}`) + } + } + + const is_production = env.NODE_ENV === "production" + + const developmentDefault = ( + str: EnvironmentValue, + name: string, + defaultValue: string + ): string => { + if (is_production) return throwIfUndefined(str, name) + return defaultIfUndefined(str, defaultValue) + } + + validNodeEnvs(env.NODE_ENV) + + const config: Config = { + // port: env.PORT ? parseInt(env.PORT) : 3000, + jwt_secret: env.JWT_SECRET || "myjwtsecret", + drift_home: env.DRIFT_HOME || "~/.drift", + is_production, + memory_db: stringToBoolean(env.MEMORY_DB), + enable_admin: stringToBoolean(env.ENABLE_ADMIN), + secret_key: developmentDefault(env.SECRET_KEY, "SECRET_KEY", "secret"), + registration_password: env.REGISTRATION_PASSWORD ?? "", + welcome_content: env.WELCOME_CONTENT ?? "", + welcome_title: env.WELCOME_TITLE ?? "", + url: 'http://localhost:3000' + } + return config +} + + +export default config(process.env) diff --git a/client/lib/get-title-for-post-copy.ts b/client/lib/get-title-for-post-copy.ts index 87a230e5..b3a2b479 100644 --- a/client/lib/get-title-for-post-copy.ts +++ b/client/lib/get-title-for-post-copy.ts @@ -14,7 +14,9 @@ const replaceLastInString = ( ) } -const getTitleForPostCopy = (title: string) => { +const getTitleForPostCopy = (title?: string) => { + if (!title) return "" + const numberAtEndOfTitle = title.split(" ").pop() if (numberAtEndOfTitle) { const number = parseInt(numberAtEndOfTitle) diff --git a/client/lib/hooks/use-user-data.ts b/client/lib/hooks/use-user-data.ts index 455c26fc..3cf93992 100644 --- a/client/lib/hooks/use-user-data.ts +++ b/client/lib/hooks/use-user-data.ts @@ -1,7 +1,7 @@ import { User } from "@lib/types" import Cookies from "js-cookie" -import { useRouter } from "next/router" -import { useEffect, useMemo, useState } from "react" +import { useRouter } from "next/navigation" +import { useEffect, useState } from "react" const useUserData = () => { const [authToken, setAuthToken] = useState( diff --git a/client/lib/render-markdown.tsx b/client/lib/render-markdown.tsx new file mode 100644 index 00000000..baa9c2a1 --- /dev/null +++ b/client/lib/render-markdown.tsx @@ -0,0 +1,189 @@ +import { marked } from "marked" +// import Highlight, { defaultProps, Language } from "prism-react-renderer" +import { renderToStaticMarkup } from "react-dom/server" +import Image from "next/image" +import Link from "next/link" + +// // image sizes. DDoS Safe? +// const imageSizeLink = /^!?\[((?:\[[^\[\]]*\]|\\[\[\]]?|`[^`]*`|[^\[\]\\])*?)\]\(\s*(<(?:\\[<>]?|[^\s<>\\])*>|(?:\\[()]?|\([^\s\x00-\x1f()\\]*\)|[^\s\x00-\x1f()\\])*?(?:\s+=(?:[\w%]+)?x(?:[\w%]+)?)?)(?:\s+("(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)))?\s*\)/; +// //@ts-ignore +// Lexer.rules.inline.normal.link = imageSizeLink; +// //@ts-ignore +// Lexer.rules.inline.gfm.link = imageSizeLink; +// //@ts-ignore +// Lexer.rules.inline.breaks.link = imageSizeLink; + +//@ts-ignore +// delete defaultProps.theme +// import linkStyles from '../components/link/link.module.css' + +const renderer = new marked.Renderer() + +// @ts-ignore +renderer.heading = (text, level, _, slugger) => { + const id = slugger.slug(text) + const Component = `h${level}` + + return ( +

+ +

+ ) +} + +// renderer.link = (href, _, text) => { +// const isHrefLocal = href?.startsWith('/') || href?.startsWith('#') +// if (isHrefLocal) { +// return renderToStaticMarkup( +// +// {text} +// +// ) +// } + +// // dirty hack +// // if text contains elements, render as html +// return +// } + +// @ts-ignore +renderer.image = function (href, _, text) { + return ${text} +} + +renderer.checkbox = () => "" +// @ts-ignore +renderer.listitem = (text, task, checked) => { + if (task) { + return ( +
  • + + ​ + + + ${text} +
  • + ) + } + + return `
  • ${text}
  • ` +} + +//@ts-ignore +renderer.code = (code: string, language: string) => { + return ( +
    +			{/* {title && {title} } */}
    +			{/* {language && title &&  {language} } */}
    +			
    +		
    + ) +} + +marked.setOptions({ + gfm: true, + breaks: true, + headerIds: true, + renderer +}) + +const markdown = (markdown: string) => marked(markdown) + +export default markdown + +const Code = ({ + code, + language, + highlight, + title, + ...props +}: { + code: string + language: string + highlight?: string + title?: string +}) => { + if (!language) + return ( + <> + + + ) + + const highlightedLines = highlight + ? //@ts-ignore + highlight.split(",").reduce((lines, h) => { + if (h.includes("-")) { + // Expand ranges like 3-5 into [3,4,5] + const [start, end] = h.split("-").map(Number) + const x = Array(end - start + 1) + .fill(undefined) + .map((_, i) => i + start) + return [...lines, ...x] + } + + return [...lines, Number(h)] + }, []) + : "" + + // https://mdxjs.com/guides/syntax-harkedighlighting#all-together + return ( + <> + {/* + {({ + className, + style, + tokens, + getLineProps, + getTokenProps + }: { + className: string + style: any + tokens: any + getLineProps: any + getTokenProps: any + }) => ( + + {tokens.map((line: string[], i: number) => ( +
    + {line.map((token: string, key: number) => ( + + ))} +
    + ))} +
    + )} +
    */} + <> + + + + ) +} diff --git a/client/middleware.ts b/client/middleware.ts index 0e4c6f26..f938bc1f 100644 --- a/client/middleware.ts +++ b/client/middleware.ts @@ -65,7 +65,7 @@ export function middleware(req: NextRequest, event: NextFetchEvent) { export const config = { match: [ "/signout", - "/", + // "/", "/signin", "/signup", "/new", diff --git a/client/package.json b/client/package.json index 85013d32..a419e587 100644 --- a/client/package.json +++ b/client/package.json @@ -3,25 +3,31 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --port 3001", + "dev": "next dev --port 3000", "build": "next build", - "start": "next start --port 3001", + "start": "next start --port 3000", "lint": "next lint && prettier --list-different --config .prettierrc '{components,lib,pages}/**/*.{ts,tsx}' --write", "analyze": "cross-env ANALYZE=true next build", - "find:unused": "next-unused" + "find:unused": "next-unused", + "prisma": "prisma" }, "dependencies": { "@geist-ui/core": "^2.3.8", "@geist-ui/icons": "1.0.2", + "@prisma/client": "^4.6.0", "@types/cookie": "0.5.1", "@types/js-cookie": "3.0.2", + "bcrypt": "^5.1.0", "client-zip": "2.2.1", "clsx": "^1.2.1", "cookie": "0.5.0", "dotenv": "16.0.0", "js-cookie": "3.0.1", + "jsonwebtoken": "^8.5.1", + "marked": "^4.2.2", "next": "13.0.3-canary.2", "next-themes": "npm:@wits/next-themes@0.2.7", + "prism-react-renderer": "^1.3.5", "rc-table": "7.24.1", "react": "18.2.0", "react-datepicker": "4.8.0", @@ -30,10 +36,14 @@ "react-hot-toast": "^2.4.0", "react-loading-skeleton": "3.1.0", "swr": "1.3.0", - "textarea-markdown-editor": "0.1.13" + "textarea-markdown-editor": "0.1.13", + "zod": "^3.19.1" }, "devDependencies": { "@next/bundle-analyzer": "12.1.6", + "@types/bcrypt": "^5.0.0", + "@types/jsonwebtoken": "^8.5.9", + "@types/marked": "^4.0.7", "@types/node": "17.0.23", "@types/react": "18.0.9", "@types/react-datepicker": "4.4.1", @@ -43,6 +53,7 @@ "eslint-config-next": "13.0.2", "next-unused": "0.0.6", "prettier": "2.6.2", + "prisma": "^4.6.0", "typescript": "4.6.4", "typescript-plugin-css-modules": "3.4.0" }, @@ -59,8 +70,5 @@ "components", "lib" ] - }, - "overrides": { - "next": "13.0.2" } } diff --git a/client/pages/_document.tsx b/client/pages/_document.tsx index f1fb1551..18ca6fe3 100644 --- a/client/pages/_document.tsx +++ b/client/pages/_document.tsx @@ -1,4 +1,4 @@ -import { CssBaseline } from "@geist-ui/core" +import { CssBaseline } from "@geist-ui/core/dist" import Document, { Html, Head, diff --git a/client/pages/admin.tsx b/client/pages/admin.tsx index d91c928f..dc63d36f 100644 --- a/client/pages/admin.tsx +++ b/client/pages/admin.tsx @@ -1,6 +1,6 @@ import styles from "@styles/Home.module.css" -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" import { useEffect } from "react" import Admin from "@components/admin" import useSignedIn from "@lib/hooks/use-signed-in" diff --git a/client/pages/api/auth/backup.ts b/client/pages/api/auth/backup.ts new file mode 100644 index 00000000..634e13d9 --- /dev/null +++ b/client/pages/api/auth/backup.ts @@ -0,0 +1,32 @@ +import config from "@lib/config" +import { NextApiRequest, NextApiResponse } from "next" + +const handleRequiresPasscode = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + const requiresPasscode = Boolean(config.registration_password) + return res.json({ requiresPasscode }) +} + +const PATH_TO_HANDLER = { + "requires-passcode": handleRequiresPasscode +} + +// eslint-disable-next-line import/no-anonymous-default-export +export default (req: NextApiRequest, res: NextApiResponse) => { + const { slug } = req.query + + if (!slug || Array.isArray(slug)) { + return res.status(400).json({ error: "Missing param" }) + } + + switch (req.method) { + case "GET": + if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) { + return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res) + } + default: + return res.status(405).json({ error: "Method not allowed" }) + } +} diff --git a/client/pages/api/auth/requires-passcode.ts b/client/pages/api/auth/requires-passcode.ts new file mode 100644 index 00000000..174aedba --- /dev/null +++ b/client/pages/api/auth/requires-passcode.ts @@ -0,0 +1,35 @@ +import config from "@lib/config" +import { NextApiRequest, NextApiResponse } from "next" + + +export const getRequiresPasscode = async () => { + const requiresPasscode = Boolean(config.registration_password) + return requiresPasscode +} + +const handleRequiresPasscode = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + return res.json({ requiresPasscode: await getRequiresPasscode() }) +} + +export default async function requiresPasscode( + req: NextApiRequest, + res: NextApiResponse +) { + const { slug } = req.query + + if (!slug || Array.isArray(slug)) { + return res.status(400).json({ error: "Missing param" }) + } + + switch (req.method) { + case "GET": + if (slug === "requires-passcode") { + return handleRequiresPasscode(req, res) + } + default: + return res.status(405).json({ error: "Method not allowed" }) + } +} diff --git a/client/pages/api/auth/signin.ts b/client/pages/api/auth/signin.ts new file mode 100644 index 00000000..f11e07fb --- /dev/null +++ b/client/pages/api/auth/signin.ts @@ -0,0 +1,36 @@ +import config from "@lib/config" +import { NextApiRequest, NextApiResponse } from "next" +import prisma from "app/prisma" +import bcrypt from "bcrypt" +import { generateAccessToken } from "@lib/api/generate-access-token" +import Cookies from "js-cookie" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { username, password } = req.body + if (!username || !password) { + return res.status(400).json({ error: "Missing param" }) + } + + const user = await prisma.user.findFirst({ + where: { + username + } + }) + + if (!user) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const isPasswordValid = await bcrypt.compare(password, user.password) + if (!isPasswordValid) { + return res.status(401).json({ error: "Unauthorized" }) + } + + const token = await generateAccessToken(user) + Cookies.set("drift-user", user.id, { path: "/" }) + Cookies.set("drift-token", token, { path: "/" }) + return res.status(201).json({ token: token, userId: user.id }) +} diff --git a/client/pages/api/auth/signup.ts b/client/pages/api/auth/signup.ts new file mode 100644 index 00000000..3f4bf2e5 --- /dev/null +++ b/client/pages/api/auth/signup.ts @@ -0,0 +1,45 @@ +import config from "@lib/config" +import { NextApiRequest, NextApiResponse } from "next" +import prisma from "app/prisma" +import bcrypt, { genSalt } from "bcrypt" +import { generateAccessToken } from "@lib/api/generate-access-token" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const { username, password, serverPassword } = req.body + if (!username || !password) { + return res.status(400).json({ error: "Missing param" }) + } + + if ( + config.registration_password && + serverPassword !== config.registration_password + ) { + console.log("Registration password mismatch") + return res.status(401).json({ error: "Unauthorized" }) + } + + const salt = await genSalt(10) + + // the first user is the admin + const isUserAdminByDefault = config.enable_admin && (await prisma.user.count()) === 0 + const userRole = isUserAdminByDefault ? "admin" : "user" + + const user = await prisma.user.create({ + data: { + username, + password: await bcrypt.hash(password, salt), + role: userRole + }, + }) + + const token = await generateAccessToken(user) + + return res.status(201).json({ token: token, userId: user.id }) +} + + + + diff --git a/client/pages/api/file/get-html.ts b/client/pages/api/file/get-html.ts new file mode 100644 index 00000000..f5bf4e9e --- /dev/null +++ b/client/pages/api/file/get-html.ts @@ -0,0 +1,52 @@ +import getHtmlFromFile from "@lib/api/get-html-from-drift-file" +import { parseUrlQuery } from "@lib/api/parse-url-query" +import prisma from "app/prisma" +import { NextApiRequest, NextApiResponse } from "next" + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + switch (req.method) { + case "GET": + const query = req.query + const fileId = parseUrlQuery(query.fileId) + const content = parseUrlQuery(query.content) + const title = parseUrlQuery(query.title) + + if (fileId && (content || title)) { + return res.status(400).json({ error: "Too many arguments" }) + } + + if (fileId) { + // TODO: abstract to getFileById + const file = await prisma.file.findUnique({ + where: { + id: fileId + } + }) + + if (!file) { + return res.status(404).json({ error: "File not found" }) + } + + return res.json(file.html) + } else { + if (!content || !title) { + return res.status(400).json({ error: "Missing arguments" }) + } + + const renderedHTML = getHtmlFromFile({ + title, + content + }) + + res.setHeader("Content-Type", "text/plain") + res.status(200).write(renderedHTML) + res.end() + return + } + default: + return res.status(405).json({ error: "Method not allowed" }) + } +} diff --git a/client/pages/api/html/[id].ts b/client/pages/api/file/html/[id].ts similarity index 100% rename from client/pages/api/html/[id].ts rename to client/pages/api/file/html/[id].ts diff --git a/client/pages/api/user/[slug].ts b/client/pages/api/user/[slug].ts new file mode 100644 index 00000000..e4089d48 --- /dev/null +++ b/client/pages/api/user/[slug].ts @@ -0,0 +1,32 @@ +import { withJwt } from "@lib/api/jwt" +import config from "@lib/config" +import { NextApiRequest, NextApiResponse } from "next" + +const handleSelf = async ( + req: NextApiRequest, + res: NextApiResponse +) => { + +} + +const PATH_TO_HANDLER = { + "self": handleRequiresPasscode +} + +// eslint-disable-next-line import/no-anonymous-default-export +export default withJwt((req: NextApiRequest, res: NextApiResponse) => { + const { slug } = req.query + + if (!slug || Array.isArray(slug)) { + return res.status(400).json({ error: "Missing param" }) + } + + switch (req.method) { + case "GET": + if (PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER]) { + return PATH_TO_HANDLER[slug as keyof typeof PATH_TO_HANDLER](req, res) + } + default: + return res.status(405).json({ error: "Method not allowed" }) + } +}) diff --git a/client/pages/api/welcome.ts b/client/pages/api/welcome.ts new file mode 100644 index 00000000..6915a935 --- /dev/null +++ b/client/pages/api/welcome.ts @@ -0,0 +1,34 @@ +// a nextjs api handerl + +import config from "@lib/config" +import markdown from "@lib/render-markdown" +import { NextApiRequest, NextApiResponse } from "next" + +export const getWelcomeContent = async () => { + const introContent = config.welcome_content + const introTitle = config.welcome_title + // if (!introContent || !introTitle) { + // return {} + // } + + console.log(introContent) + + + return { + title: introTitle, + content: introContent, + rendered: markdown(introContent) + } +} + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const welcomeContent = await getWelcomeContent() + if (!welcomeContent) { + return res.status(500).json({ error: "Missing welcome content" }) + } + + return res.json(welcomeContent) +} diff --git a/client/pages/expired.tsx b/client/pages/expired.tsx index 43670c65..6f7d16c0 100644 --- a/client/pages/expired.tsx +++ b/client/pages/expired.tsx @@ -1,4 +1,4 @@ -import { Note, Page, Text } from "@geist-ui/core" +import { Note, Page, Text } from "@geist-ui/core/dist" import styles from "@styles/Home.module.css" const Expired = () => { diff --git a/client/pages/index.backup.tsx b/client/pages/index.backup.tsx index e560ed4a..cc9c7642 100644 --- a/client/pages/index.backup.tsx +++ b/client/pages/index.backup.tsx @@ -1,7 +1,7 @@ import styles from "@styles/Home.module.css" import PageSeo from "@components/page-seo" import HomeComponent from "@components/home" -import { Page, Text } from "@geist-ui/core" +import { Page, Text } from "@geist-ui/core/dist" import type { GetStaticProps } from "next" import { InferGetStaticPropsType } from "next" type Props = diff --git a/client/pages/mine.tsx b/client/pages/mine.tsx index d4050379..d24e050f 100644 --- a/client/pages/mine.tsx +++ b/client/pages/mine.tsx @@ -5,7 +5,7 @@ import MyPosts from "@components/my-posts" import cookie from "cookie" import type { GetServerSideProps } from "next" import { Post } from "@lib/types" -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" const Home = ({ morePosts, diff --git a/client/pages/new/from/[id].tsx b/client/pages/new-backup/from/[id].tsx similarity index 97% rename from client/pages/new/from/[id].tsx rename to client/pages/new-backup/from/[id].tsx index 6f0eabfb..7a629977 100644 --- a/client/pages/new/from/[id].tsx +++ b/client/pages/new-backup/from/[id].tsx @@ -2,7 +2,7 @@ import styles from "@styles/Home.module.css" import NewPost from "@components/new-post" import Header from "@components/header" import PageSeo from "@components/page-seo" -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" import Head from "next/head" import { GetServerSideProps } from "next" import { Post } from "@lib/types" diff --git a/client/pages/new/index.tsx b/client/pages/new-backup/index.tsx similarity index 93% rename from client/pages/new/index.tsx rename to client/pages/new-backup/index.tsx index 5a2e152d..ee2fbb95 100644 --- a/client/pages/new/index.tsx +++ b/client/pages/new-backup/index.tsx @@ -2,7 +2,7 @@ import styles from "@styles/Home.module.css" import NewPost from "@components/new-post" import Header from "@components/header" import PageSeo from "@components/page-seo" -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" import Head from "next/head" const New = () => { diff --git a/client/pages/settings.tsx b/client/pages/settings.tsx index 9f2a2cb0..2e3530ed 100644 --- a/client/pages/settings.tsx +++ b/client/pages/settings.tsx @@ -7,7 +7,7 @@ import { Page, Note, Textarea -} from "@geist-ui/core" +} from "@geist-ui/core/dist" import PageSeo from "@components/page-seo" import styles from "@styles/Home.module.css" import SettingsPage from "@components/settings" diff --git a/client/pages/signin.tsx b/client/pages/signin.backup.tsx similarity index 88% rename from client/pages/signin.tsx rename to client/pages/signin.backup.tsx index 47815b21..3a8681fd 100644 --- a/client/pages/signin.tsx +++ b/client/pages/signin.backup.tsx @@ -1,4 +1,4 @@ -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" import PageSeo from "@components/page-seo" import Auth from "@components/auth" import styles from "@styles/Home.module.css" diff --git a/client/pages/signup.tsx b/client/pages/signup.backup.tsx similarity index 88% rename from client/pages/signup.tsx rename to client/pages/signup.backup.tsx index 85556f71..2ae6d8cc 100644 --- a/client/pages/signup.tsx +++ b/client/pages/signup.backup.tsx @@ -1,4 +1,4 @@ -import { Page } from "@geist-ui/core" +import { Page } from "@geist-ui/core/dist" import Auth from "@components/auth" import PageSeo from "@components/page-seo" import styles from "@styles/Home.module.css" diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index fa54156c..abf56175 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -4,12 +4,17 @@ specifiers: '@geist-ui/core': ^2.3.8 '@geist-ui/icons': 1.0.2 '@next/bundle-analyzer': 12.1.6 + '@prisma/client': ^4.6.0 + '@types/bcrypt': ^5.0.0 '@types/cookie': 0.5.1 '@types/js-cookie': 3.0.2 + '@types/jsonwebtoken': ^8.5.9 + '@types/marked': ^4.0.7 '@types/node': 17.0.23 '@types/react': 18.0.9 '@types/react-datepicker': 4.4.1 '@types/react-dom': 18.0.3 + bcrypt: ^5.1.0 client-zip: 2.2.1 clsx: ^1.2.1 cookie: 0.5.0 @@ -18,10 +23,14 @@ specifiers: eslint: 8.27.0 eslint-config-next: 13.0.2 js-cookie: 3.0.1 + jsonwebtoken: ^8.5.1 + marked: ^4.2.2 next: 13.0.3-canary.2 next-themes: npm:@wits/next-themes@0.2.7 next-unused: 0.0.6 prettier: 2.6.2 + prism-react-renderer: ^1.3.5 + prisma: ^4.6.0 rc-table: 7.24.1 react: 18.2.0 react-datepicker: 4.8.0 @@ -34,19 +43,25 @@ specifiers: textarea-markdown-editor: 0.1.13 typescript: 4.6.4 typescript-plugin-css-modules: 3.4.0 + zod: ^3.19.1 dependencies: '@geist-ui/core': 2.3.8_biqbaboplfbrettd7655fr4n2y '@geist-ui/icons': 1.0.2_zhza2kbnl2wkkf7vqdl3ton2f4 + '@prisma/client': 4.6.0_prisma@4.6.0 '@types/cookie': 0.5.1 '@types/js-cookie': 3.0.2 + bcrypt: 5.1.0 client-zip: 2.2.1 clsx: 1.2.1 cookie: 0.5.0 dotenv: 16.0.0 js-cookie: 3.0.1 + jsonwebtoken: 8.5.1 + marked: 4.2.2 next: 13.0.3-canary.2_biqbaboplfbrettd7655fr4n2y next-themes: /@wits/next-themes/0.2.7_qjr36eup74ongf7bl2iopfchwe + prism-react-renderer: 1.3.5_react@18.2.0 rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y react: 18.2.0 react-datepicker: 4.8.0_biqbaboplfbrettd7655fr4n2y @@ -56,12 +71,16 @@ dependencies: react-loading-skeleton: 3.1.0_react@18.2.0 swr: 1.3.0_react@18.2.0 textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y + zod: 3.19.1 optionalDependencies: sharp: 0.31.2 devDependencies: '@next/bundle-analyzer': 12.1.6 + '@types/bcrypt': 5.0.0 + '@types/jsonwebtoken': 8.5.9 + '@types/marked': 4.0.7 '@types/node': 17.0.23 '@types/react': 18.0.9 '@types/react-datepicker': 4.4.1_biqbaboplfbrettd7655fr4n2y @@ -71,6 +90,7 @@ devDependencies: eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i next-unused: 0.0.6 prettier: 2.6.2 + prisma: 4.6.0 typescript: 4.6.4 typescript-plugin-css-modules: 3.4.0_typescript@4.6.4 @@ -175,6 +195,24 @@ packages: resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} dev: true + /@mapbox/node-pre-gyp/1.0.10: + resolution: {integrity: sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA==} + hasBin: true + dependencies: + detect-libc: 2.0.1 + https-proxy-agent: 5.0.1 + make-dir: 3.1.0 + node-fetch: 2.6.7 + nopt: 5.0.0 + npmlog: 5.0.1 + rimraf: 3.0.2 + semver: 7.3.8 + tar: 6.1.12 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /@next/bundle-analyzer/12.1.6: resolution: {integrity: sha512-WLydwytAeHoC/neXsiIgK+a6Me12PuSpwopnsZgX5JFNwXQ9MlwPeMGS3aTZkYsv8QmSm0Ns9Yh9FkgLKYaUuQ==} dependencies: @@ -339,6 +377,28 @@ packages: /@popperjs/core/2.11.6: resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} + /@prisma/client/4.6.0_prisma@4.6.0: + resolution: {integrity: sha512-D9LaQinDxOHinRpcJTw2tjMtjhc9HTP+aF1IRd2oLldp/8TiwIfxK8x17OhBBiX4y1PzbJXXET7kS+5wB3es/w==} + engines: {node: '>=14.17'} + requiresBuild: true + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + dependencies: + '@prisma/engines-version': 4.6.0-53.2e719efb80b56a3f32d18a62489de95bb9c130e3 + prisma: 4.6.0 + dev: false + + /@prisma/engines-version/4.6.0-53.2e719efb80b56a3f32d18a62489de95bb9c130e3: + resolution: {integrity: sha512-0CTnfEuUbLlO6n1fM89ERDbSwI4LoyZn+1OKVSwG+aVqohj34+mXRfwOWIM0ONtYtLGGBpddvQAnAZkg+cgS6g==} + dev: false + + /@prisma/engines/4.6.0: + resolution: {integrity: sha512-S+72PAl0zTCbIGou1uXD/McvzdtP+bjOs0LRmGZfcOQcVqR9x/0f6Z+dqpUU0zIcqHEl+0DOB8UXaTwRvssFsQ==} + requiresBuild: true + /@rushstack/eslint-patch/1.2.0: resolution: {integrity: sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==} dev: true @@ -349,6 +409,12 @@ packages: tslib: 2.4.1 dev: false + /@types/bcrypt/5.0.0: + resolution: {integrity: sha512-agtcFKaruL8TmcvqbndlqHPSJgsolhf/qPWchFlgnW1gECTN/nKbFcoFnvKAQRFfKbh+BO6A3SWdJu9t+xF3Lw==} + dependencies: + '@types/node': 17.0.23 + dev: true + /@types/cookie/0.5.1: resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} dev: false @@ -361,6 +427,16 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true + /@types/jsonwebtoken/8.5.9: + resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} + dependencies: + '@types/node': 17.0.23 + dev: true + + /@types/marked/4.0.7: + resolution: {integrity: sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==} + dev: true + /@types/node/17.0.23: resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} dev: true @@ -507,6 +583,10 @@ packages: react-dom: 18.2.0_react@18.2.0 dev: false + /abbrev/1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /acorn-jsx/5.3.2_acorn@8.8.1: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -526,6 +606,15 @@ packages: hasBin: true dev: true + /agent-base/6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + dependencies: + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /ajv/6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -538,7 +627,6 @@ packages: /ansi-regex/5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - dev: true /ansi-styles/3.2.1: resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} @@ -566,6 +654,18 @@ packages: resolution: {integrity: sha512-gkco+qxENJV+8vFcDiiFhuoSvRXb2a/QPqpSoWhVz829VNJfOTnELbBmPmNKFxf3xdNnw4DWCkzkDaavcX/1YQ==} dev: true + /aproba/2.0.0: + resolution: {integrity: sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==} + dev: false + + /are-we-there-yet/2.0.0: + resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==} + engines: {node: '>=10'} + dependencies: + delegates: 1.0.0 + readable-stream: 3.6.0 + dev: false + /argparse/2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} dev: true @@ -649,11 +749,22 @@ packages: /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true /base64-js/1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + /bcrypt/5.1.0: + resolution: {integrity: sha512-RHBS7HI5N5tEnGTmtR/pppX0mmDSBpQ4aCBsj7CEQfYXDcO74A8sIBYcJMuCsis2E81zDxeENYhv66oZwLiA+Q==} + engines: {node: '>= 10.0.0'} + requiresBuild: true + dependencies: + '@mapbox/node-pre-gyp': 1.0.10 + node-addon-api: 5.0.0 + transitivePeerDependencies: + - encoding + - supports-color + dev: false + /big.js/3.2.0: resolution: {integrity: sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==} dev: true @@ -679,7 +790,6 @@ packages: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true /braces/3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -688,6 +798,10 @@ packages: fill-range: 7.0.1 dev: true + /buffer-equal-constant-time/1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer/5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -747,6 +861,11 @@ packages: dev: false optional: true + /chownr/2.0.0: + resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} + engines: {node: '>=10'} + dev: false + /classnames/2.3.2: resolution: {integrity: sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==} dev: false @@ -808,6 +927,11 @@ packages: dev: false optional: true + /color-support/1.1.3: + resolution: {integrity: sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==} + hasBin: true + dev: false + /color/4.2.3: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} @@ -832,7 +956,10 @@ packages: /concat-map/0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + + /console-control-strings/1.1.0: + resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} + dev: false /cookie/0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} @@ -954,7 +1081,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decode-uri-component/0.2.0: resolution: {integrity: sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==} @@ -991,6 +1117,10 @@ packages: object-keys: 1.1.1 dev: true + /delegates/1.0.0: + resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==} + dev: false + /dependency-tree/8.1.2: resolution: {integrity: sha512-c4CL1IKxkKng0oT5xrg4uNiiMVFqTGOXqHSFx7XEFdgSsp6nw3AGGruICppzJUrfad/r7GLqt26rmWU4h4j39A==} engines: {node: ^10.13 || ^12 || >=14} @@ -1009,7 +1139,6 @@ packages: resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==} engines: {node: '>=8'} dev: false - optional: true /detective-amd/3.1.2: resolution: {integrity: sha512-jffU26dyqJ37JHR/o44La6CxtrDf3Rt9tvd2IbImJYxWKTMdBjctp37qoZ6ZcY80RHg+kzWz4bXn39e4P7cctQ==} @@ -1139,6 +1268,16 @@ packages: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} dev: true + /ecdsa-sig-formatter/1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + + /emoji-regex/8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + dev: false + /emoji-regex/9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} dev: true @@ -1646,9 +1785,15 @@ packages: dev: false optional: true + /fs-minipass/2.1.0: + resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.4 + dev: false + /fs.realpath/1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true /fsevents/2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} @@ -1676,6 +1821,21 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /gauge/3.0.2: + resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==} + engines: {node: '>=10'} + dependencies: + aproba: 2.0.0 + color-support: 1.1.3 + console-control-strings: 1.1.0 + has-unicode: 2.0.1 + object-assign: 4.1.1 + signal-exit: 3.0.7 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wide-align: 1.1.5 + dev: false + /generic-names/1.0.3: resolution: {integrity: sha512-b6OHfQuKasIKM9b6YPkX+KUj/TLBTx3B/1aT1T5F12FEuEqyFMdr59OMS53aoaSw8eVtapdqieX6lbg5opaOhA==} dependencies: @@ -1749,7 +1909,6 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true /globals/13.17.0: resolution: {integrity: sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==} @@ -1838,6 +1997,10 @@ packages: has-symbols: 1.0.3 dev: true + /has-unicode/2.0.1: + resolution: {integrity: sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==} + dev: false + /has/1.0.3: resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} engines: {node: '>= 0.4.0'} @@ -1845,6 +2008,16 @@ packages: function-bind: 1.1.1 dev: true + /https-proxy-agent/5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + dependencies: + agent-base: 6.0.2 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /iconv-lite/0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -1910,7 +2083,6 @@ packages: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true /inherits/2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -1976,6 +2148,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /is-fullwidth-code-point/3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + dev: false + /is-glob/4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -2118,6 +2295,22 @@ packages: minimist: 1.2.7 dev: true + /jsonwebtoken/8.5.1: + resolution: {integrity: sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==} + engines: {node: '>=4', npm: '>=1.4.28'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 5.7.1 + dev: false + /jsx-ast-utils/3.3.3: resolution: {integrity: sha512-fYQHZTZ8jSfmWZ0iyzfwiU4WDX4HpHbMCZ3gPlWYiCl3BoeOTsqKBqnTVfH2rYT7eP5c3sVbeSPHnnJOaTrWiw==} engines: {node: '>=4.0'} @@ -2126,6 +2319,21 @@ packages: object.assign: 4.1.4 dev: true + /jwa/1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws/3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /language-subtag-registry/0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: true @@ -2206,10 +2414,38 @@ packages: resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==} dev: true + /lodash.includes/4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean/3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger/4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber/3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + + /lodash.isplainobject/4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + dev: false + + /lodash.isstring/4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + dev: false + /lodash.merge/4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once/4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash/4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} dev: true @@ -2275,6 +2511,19 @@ packages: dev: true optional: true + /make-dir/3.1.0: + resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} + engines: {node: '>=8'} + dependencies: + semver: 6.3.0 + dev: false + + /marked/4.2.2: + resolution: {integrity: sha512-JjBTFTAvuTgANXx82a5vzK9JLSMoV6V3LBVn4Uhdso6t7vXrGx7g1Cd2r6NYSsxrYbQGFCMqBDhFHyK5q2UvcQ==} + engines: {node: '>= 12'} + hasBin: true + dev: false + /memory-fs/0.5.0: resolution: {integrity: sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==} engines: {node: '>=4.3.0 <5.0.0 || >=5.10'} @@ -2319,11 +2568,25 @@ packages: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} dependencies: brace-expansion: 1.1.11 - dev: true /minimist/1.2.7: resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + /minipass/3.3.4: + resolution: {integrity: sha512-I9WPbWHCGu8W+6k1ZiGpPu0GkoKBeorkfKNuAFBNS1HNFJvke82sxvI5bzcCNpWPorkOO5QQ+zomzzwRxejXiw==} + engines: {node: '>=8'} + dependencies: + yallist: 4.0.0 + dev: false + + /minizlib/2.1.2: + resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} + engines: {node: '>= 8'} + dependencies: + minipass: 3.3.4 + yallist: 4.0.0 + dev: false + /mkdirp-classic/0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false @@ -2333,7 +2596,6 @@ packages: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true - dev: true /module-definition/3.4.0: resolution: {integrity: sha512-XxJ88R1v458pifaSkPNLUTdSPNVGMP2SXVncVmApGO+gAfrLANiYe6JofymCzVceGOMwQE2xogxBSc8uB7XegA==} @@ -2373,11 +2635,9 @@ packages: /ms/2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms/2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - dev: true /nanoid/3.3.4: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} @@ -2474,7 +2734,18 @@ packages: /node-addon-api/5.0.0: resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==} dev: false - optional: true + + /node-fetch/2.6.7: + resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false /node-source-walk/4.3.0: resolution: {integrity: sha512-8Q1hXew6ETzqKRAs3jjLioSxNfT1cx74ooiF8RlAONwVMcfq+UdzLC2eB5qcPldUxaE5w3ytLkrmV1TGddhZTA==} @@ -2483,11 +2754,28 @@ packages: '@babel/parser': 7.20.3 dev: true + /nopt/5.0.0: + resolution: {integrity: sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==} + engines: {node: '>=6'} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-path/3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} dev: true + /npmlog/5.0.1: + resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==} + dependencies: + are-we-there-yet: 2.0.0 + console-control-strings: 1.1.0 + gauge: 3.0.2 + set-blocking: 2.0.0 + dev: false + /object-assign/4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2640,7 +2928,6 @@ packages: /path-is-absolute/1.0.1: resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} engines: {node: '>=0.10.0'} - dev: true /path-key/3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} @@ -2843,6 +3130,22 @@ packages: parse-ms: 2.1.0 dev: true + /prism-react-renderer/1.3.5_react@18.2.0: + resolution: {integrity: sha512-IJ+MSwBWKG+SM3b2SUfdrhC+gu01QkV2KmRQgREThBfSQRoufqRfxfHUxpG1WcaFjP+kojcFyO9Qqtpgt3qLCg==} + peerDependencies: + react: '>=0.14.9' + dependencies: + react: 18.2.0 + dev: false + + /prisma/4.6.0: + resolution: {integrity: sha512-TAnObUMGCM9NLt9nsRs1WWYQGPKsJOK8bN/7gSAnBcYIxMCFFDe+XtFYJbyTzsJZ/i+0rH4zg8au3m7HX354LA==} + engines: {node: '>=14.17'} + hasBin: true + requiresBuild: true + dependencies: + '@prisma/engines': 4.6.0 + /process-nextick-args/2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} dev: true @@ -3142,7 +3445,6 @@ packages: hasBin: true dependencies: glob: 7.2.3 - dev: true /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3199,13 +3501,10 @@ packages: /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true - dev: true - optional: true /semver/6.3.0: resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} hasBin: true - dev: true /semver/7.3.8: resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} @@ -3214,6 +3513,10 @@ packages: dependencies: lru-cache: 6.0.0 + /set-blocking/2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + dev: false + /shallowequal/1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} dev: false @@ -3256,7 +3559,6 @@ packages: /signal-exit/3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - dev: true /simple-concat/1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} @@ -3323,6 +3625,15 @@ packages: engines: {node: '>= 8'} dev: true + /string-width/4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + dev: false + /string.prototype.matchall/4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} dependencies: @@ -3377,7 +3688,6 @@ packages: engines: {node: '>=8'} dependencies: ansi-regex: 5.0.1 - dev: true /strip-bom/3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} @@ -3496,6 +3806,18 @@ packages: dev: false optional: true + /tar/6.1.12: + resolution: {integrity: sha512-jU4TdemS31uABHd+Lt5WEYJuzn+TJTCBLljvIAHZOz6M9Os5pJ4dD+vRFLxPa/n3T0iEFzpi+0x1UfuDZYbRMw==} + engines: {node: '>=10'} + dependencies: + chownr: 2.0.0 + fs-minipass: 2.1.0 + minipass: 3.3.4 + minizlib: 2.1.2 + mkdirp: 1.0.4 + yallist: 4.0.0 + dev: false + /temp/0.4.0: resolution: {integrity: sha512-IsFisGgDKk7qzK9erMIkQe/XwiSUdac7z3wYOsjcLkhPBy3k1SlvLoIh2dAHIlEpgA971CgguMrx9z8fFg7tSA==} engines: {'0': node >=0.4.0} @@ -3534,6 +3856,10 @@ packages: engines: {node: '>=6'} dev: true + /tr46/0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /ts-loader/7.0.5_typescript@4.6.4: resolution: {integrity: sha512-zXypEIT6k3oTc+OZNx/cqElrsbBtYqDknf48OZos0NQ3RTt045fBIU8RRSu+suObBzYB355aIPGOe/3kj9h7Ig==} engines: {node: '>=10.0.0'} @@ -3697,6 +4023,10 @@ packages: defaults: 1.0.4 dev: true + /webidl-conversions/3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + /webpack-bundle-analyzer/4.3.0: resolution: {integrity: sha512-J3TPm54bPARx6QG8z4cKBszahnUglcv70+N+8gUqv2I5KOFHJbzBiLx+pAp606so0X004fxM7hqRu10MLjJifA==} engines: {node: '>= 10.13.0'} @@ -3716,6 +4046,13 @@ packages: - utf-8-validate dev: true + /whatwg-url/5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive/1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -3734,6 +4071,12 @@ packages: isexe: 2.0.0 dev: true + /wide-align/1.1.5: + resolution: {integrity: sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==} + dependencies: + string-width: 4.2.3 + dev: false + /word-wrap/1.2.3: resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} engines: {node: '>=0.10.0'} @@ -3767,3 +4110,7 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} dev: true + + /zod/3.19.1: + resolution: {integrity: sha512-LYjZsEDhCdYET9ikFu6dVPGp2YH9DegXjdJToSzD9rO6fy4qiRYFoyEYwps88OseJlPyl2NOe2iJuhEhL7IpEA==} + dev: false diff --git a/client/prisma/migrations/20221110002714_init/migration.sql b/client/prisma/migrations/20221110002714_init/migration.sql new file mode 100644 index 00000000..ec85bc89 --- /dev/null +++ b/client/prisma/migrations/20221110002714_init/migration.sql @@ -0,0 +1,73 @@ +-- CreateTable +CREATE TABLE "AuthTokens" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiredReason" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME NOT NULL, + "userId" TEXT NOT NULL, + + PRIMARY KEY ("id", "token") +); + +-- CreateTable +CREATE TABLE "SequelizeMeta" ( + "name" TEXT NOT NULL PRIMARY KEY +); + +-- CreateTable +CREATE TABLE "Files" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT, + "content" TEXT, + "sha" TEXT, + "html" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "PostAuthors" ( + "id" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "postId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + PRIMARY KEY ("id", "postId", "userId") +); + +-- CreateTable +CREATE TABLE "Posts" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "visibility" TEXT NOT NULL, + "password" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "expiresAt" DATETIME, + "parentId" TEXT, + "description" TEXT +); + +-- CreateTable +CREATE TABLE "Users" ( + "id" TEXT NOT NULL PRIMARY KEY, + "username" TEXT NOT NULL, + "password" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "role" TEXT DEFAULT 'user', + "email" TEXT, + "displayName" TEXT, + "bio" TEXT +); + +-- CreateIndex +CREATE UNIQUE INDEX "AuthTokens_id_token_key" ON "AuthTokens"("id", "token"); diff --git a/client/prisma/migrations/20221110003646_auth_token_type_fixes/migration.sql b/client/prisma/migrations/20221110003646_auth_token_type_fixes/migration.sql new file mode 100644 index 00000000..3d546d34 --- /dev/null +++ b/client/prisma/migrations/20221110003646_auth_token_type_fixes/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_AuthTokens" ( + "id" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expiredReason" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "userId" TEXT NOT NULL, + + PRIMARY KEY ("id", "token") +); +INSERT INTO "new_AuthTokens" ("createdAt", "deletedAt", "expiredReason", "id", "token", "updatedAt", "userId") SELECT "createdAt", "deletedAt", "expiredReason", "id", "token", "updatedAt", "userId" FROM "AuthTokens"; +DROP TABLE "AuthTokens"; +ALTER TABLE "new_AuthTokens" RENAME TO "AuthTokens"; +CREATE UNIQUE INDEX "AuthTokens_id_token_key" ON "AuthTokens"("id", "token"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/client/prisma/migrations/20221110014822_file_optional_fixes/migration.sql b/client/prisma/migrations/20221110014822_file_optional_fixes/migration.sql new file mode 100644 index 00000000..3ecd4790 --- /dev/null +++ b/client/prisma/migrations/20221110014822_file_optional_fixes/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - Made the column `content` on table `Files` required. This step will fail if there are existing NULL values in that column. + - Made the column `html` on table `Files` required. This step will fail if there are existing NULL values in that column. + - Made the column `sha` on table `Files` required. This step will fail if there are existing NULL values in that column. + - Made the column `title` on table `Files` required. This step will fail if there are existing NULL values in that column. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Files" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "content" TEXT NOT NULL, + "sha" TEXT NOT NULL, + "html" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "deletedAt" DATETIME, + "userId" TEXT NOT NULL, + "postId" TEXT NOT NULL +); +INSERT INTO "new_Files" ("content", "createdAt", "deletedAt", "html", "id", "postId", "sha", "title", "updatedAt", "userId") SELECT "content", "createdAt", "deletedAt", "html", "id", "postId", "sha", "title", "updatedAt", "userId" FROM "Files"; +DROP TABLE "Files"; +ALTER TABLE "new_Files" RENAME TO "Files"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/client/prisma/migrations/migration_lock.toml b/client/prisma/migrations/migration_lock.toml new file mode 100644 index 00000000..e5e5c470 --- /dev/null +++ b/client/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/client/prisma/schema.prisma b/client/prisma/schema.prisma new file mode 100644 index 00000000..aaaeecf9 --- /dev/null +++ b/client/prisma/schema.prisma @@ -0,0 +1,93 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model AuthTokens { + id String @default(cuid()) + token String + expiredReason String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + userId String + // TODO: verify this isn't necessary / is replaced by an implicit m-n relation + // users User[] @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@id([id, token]) + // make id and token keys + @@unique([id, token]) +} + +model SequelizeMeta { + name String @id +} + +model File { + id String @id @default(cuid()) + title String + content String + sha String + html String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + userId String + postId String + // posts Post[] @relation(fields: [postId], references: [id], onDelete: Cascade) + // users User[] @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@map("Files") +} + +model PostToAuthors { + id String @default(cuid()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + postId String + userId String + // users User[] @relation(fields: [userId], references: [id], onDelete: Cascade) + // posts Post[] @relation(fields: [postId], references: [id], onDelete: Cascade) + + @@id([id, postId, userId]) + @@map("PostAuthors") +} + +model Post { + id String @id @default(cuid()) + title String + visibility String + password String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + expiresAt DateTime? + parentId String? + description String? + // files File[] + // postToAuthors PostToAuthors[] + + @@map("Posts") +} + +model User { + id String @id @default(cuid()) + username String + password String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + deletedAt DateTime? + role String? @default("user") + email String? + displayName String? + bio String? + // AuthTokens AuthTokens[] + // files File[] + // post_authors PostToAuthors[] + + @@map("Users") +} diff --git a/client/public/css/react-datepicker.css b/client/styles/react-datepicker.css similarity index 100% rename from client/public/css/react-datepicker.css rename to client/styles/react-datepicker.css