use next-auth, add sign in via github, switch to postgres

This commit is contained in:
Max Leiter 2022-11-11 19:17:44 -08:00
parent 7c761eb727
commit 60d1b031f5
56 changed files with 824 additions and 710 deletions

View file

@ -1,5 +1,11 @@
import Auth from "@components/auth" import Auth from "@components/auth"
import Header from "@components/header"
export default function SignInPage() { export default function SignInPage() {
return <Auth page="signin" /> return (
<>
<Header />
<Auth page="signin" />
</>
)
} }

View file

@ -1,4 +1,5 @@
import Auth from "@components/auth" import Auth from "@components/auth"
import Header from "@components/header"
import { getRequiresPasscode } from "pages/api/auth/requires-passcode" import { getRequiresPasscode } from "pages/api/auth/requires-passcode"
const getPasscode = async () => { const getPasscode = async () => {
@ -7,5 +8,10 @@ const getPasscode = async () => {
export default async function SignUpPage() { export default async function SignUpPage() {
const requiresPasscode = await getPasscode() const requiresPasscode = await getPasscode()
return <Auth page="signup" requiresServerPassword={requiresPasscode} /> return (
<>
<Header />
<Auth page="signup" requiresServerPassword={requiresPasscode} />
</>
)
} }

View file

@ -24,6 +24,7 @@ const Home = ({
width={48} width={48}
height={48} height={48}
alt="" alt=""
priority
/> />
</ShiftBy> </ShiftBy>
<Spacer /> <Spacer />

View file

@ -1,3 +1,5 @@
import Header from "@components/header"
import { getCurrentUser } from "@lib/server/session"
import { getWelcomeContent } from "pages/api/welcome" import { getWelcomeContent } from "pages/api/welcome"
import Home from "./home" import Home from "./home"
@ -8,6 +10,12 @@ const getWelcomeData = async () => {
export default async function Page() { export default async function Page() {
const { content, rendered, title } = await getWelcomeData() const { content, rendered, title } = await getWelcomeData()
const authed = await getCurrentUser();
return <Home rendered={rendered} introContent={content} introTitle={title} /> return (
<>
<Header signedIn={Boolean(authed)}/>
<Home rendered={rendered} introContent={content} introTitle={title} />
</>
)
} }

View file

@ -1,9 +1,7 @@
import NewPost from "@components/new-post" import NewPost from "@components/new-post"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import { cookies } from "next/headers" import { getPostWithFiles } from "@lib/server/prisma"
import { TOKEN_COOKIE_NAME } from "@lib/constants" import Header from "@components/header"
import { getPostWithFiles } from "app/prisma"
import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed"
const NewFromExisting = async ({ const NewFromExisting = async ({
params params
@ -14,13 +12,6 @@ const NewFromExisting = async ({
}) => { }) => {
const { id } = params const { id } = params
const router = useRouter() const router = useRouter()
const cookieList = cookies()
useRedirectIfNotAuthed()
const driftToken = cookieList.get(TOKEN_COOKIE_NAME)
if (!driftToken) {
return router.push("/signin")
}
if (!id) { if (!id) {
return router.push("/new") return router.push("/new")
@ -28,7 +19,12 @@ const NewFromExisting = async ({
const post = await getPostWithFiles(id) const post = await getPostWithFiles(id)
return <NewPost initialPost={post} newPostParent={id} /> return (
<>
<Header signedIn />
<NewPost initialPost={post} newPostParent={id} />
</>
)
} }
export default NewFromExisting export default NewFromExisting

View file

@ -1,4 +1,11 @@
import { getCurrentUser } from "@lib/server/session"
import { redirect } from "next/navigation"
export default function NewLayout({ children }: { children: React.ReactNode }) { export default function NewLayout({ children }: { children: React.ReactNode }) {
// useRedirectIfNotAuthed() const user = getCurrentUser()
if (!user) {
return redirect("/new")
}
return <>{children}</> return <>{children}</>
} }

View file

@ -1,6 +1,10 @@
import Header from "@components/header"
import NewPost from "@components/new-post" import NewPost from "@components/new-post"
import "@styles/react-datepicker.css" import "@styles/react-datepicker.css"
const New = () => <NewPost /> const New = () => <>
<Header signedIn />
<NewPost />
</>
export default New export default New

View file

@ -0,0 +1,24 @@
import PageSeo from "@components/page-seo"
import { getPostById } from "@lib/server/prisma"
export default async function Head({
params
}: {
params: {
slug: string
}
}) {
const post = await getPostById(params.slug)
if (!post) {
return null
}
return (
<PageSeo
title={`${post.title} - Drift`}
description={post.description}
isPrivate={false}
/>
)
}

View file

@ -1,16 +1,26 @@
import { USER_COOKIE_NAME } from "@lib/constants" import { USER_COOKIE_NAME } from "@lib/constants"
import { notFound, useRouter } from "next/navigation" import { notFound, redirect, useRouter } from "next/navigation"
import { cookies } from "next/headers" import { cookies } from "next/headers"
import { getPostsByUser } from "app/prisma" import { getPostsByUser } from "@lib/server/prisma"
import PostList from "@components/post-list" import PostList from "@components/post-list"
import { getCurrentUser } from "@lib/server/session"
import Header from "@components/header"
import { authOptions } from "@lib/server/auth"
export default async function Mine() { export default async function Mine() {
const userId = cookies().get(USER_COOKIE_NAME)?.value const userId = (await getCurrentUser())?.id
if (!userId) { if (!userId) {
return notFound() redirect(authOptions.pages?.signIn || "/new")
} }
const posts = await getPostsByUser(userId, true) const posts = await getPostsByUser(userId, true)
const hasMore = false const hasMore = false
return <PostList morePosts={hasMore} initialPosts={posts} /> return (
<>
<Header signedIn />
<PostList morePosts={hasMore} initialPosts={posts} />
</>
)
} }

View file

@ -1,5 +1,37 @@
import SettingsPage from "@components/settings" import Header from "@components/header"
import SettingsGroup from "@components/settings-group"
import Password from "@components/settings/sections/password"
import Profile from "@components/settings/sections/profile"
import { authOptions } from "@lib/server/auth"
import { getCurrentUser } from "@lib/server/session"
import { redirect } from "next/navigation"
const Settings = () => <SettingsPage /> export default async function SettingsPage() {
const user = await getCurrentUser()
export default Settings if (!user) {
redirect(authOptions.pages?.signIn || "/new")
}
return (
<>
<Header signedIn />
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
marginBottom: "var(--gap)"
}}
>
<h1>Settings</h1>
<SettingsGroup title="Profile">
<Profile user={user} />
</SettingsGroup>
<SettingsGroup title="Password">
<Password />
</SettingsGroup>
</div>
</>
)
}

View file

@ -1,18 +1,18 @@
import Admin from "@components/admin" import Admin from "@components/admin"
import { TOKEN_COOKIE_NAME } from "@lib/constants" import { TOKEN_COOKIE_NAME } from "@lib/constants"
import { isUserAdmin } from "app/prisma" import { isUserAdmin } from "@lib/server/prisma"
import { getCurrentUser } from "@lib/server/session"
import { cookies } from "next/headers" import { cookies } from "next/headers"
import { notFound } from "next/navigation" import { notFound } from "next/navigation"
const AdminPage = async () => { const AdminPage = async () => {
const driftToken = cookies().get(TOKEN_COOKIE_NAME)?.value const user = await getCurrentUser()
if (!driftToken) {
if (!user) {
return notFound() return notFound()
} }
const isAdmin = await isUserAdmin(driftToken) if (user.role !== "admin") {
if (!isAdmin) {
return notFound() return notFound()
} }

View file

@ -1,16 +1,17 @@
import "@styles/globals.css" import "@styles/globals.css"
import { ServerThemeProvider } from "next-themes" import { ServerThemeProvider } from "next-themes"
import { LayoutWrapper } from "./root-layout-wrapper" import { LayoutWrapper } from "./root-layout-wrapper"
import styles from '@styles/Home.module.css';
interface RootLayoutProps { interface RootLayoutProps {
children: React.ReactNode children: React.ReactNode
} }
export default function RootLayout({ children }: RootLayoutProps) { export default async function RootLayout({ children }: RootLayoutProps) {
return ( return (
<ServerThemeProvider <ServerThemeProvider
cookieName="drift-theme" cookieName="drift-theme"
// disableTransitionOnChange disableTransitionOnChange
attribute="data-theme" attribute="data-theme"
enableColorScheme enableColorScheme
> >
@ -50,7 +51,7 @@ export default function RootLayout({ children }: RootLayoutProps) {
<meta name="theme-color" content="#ffffff" /> <meta name="theme-color" content="#ffffff" />
<title>Drift</title> <title>Drift</title>
</head> </head>
<body> <body className={styles.main}>
<LayoutWrapper>{children}</LayoutWrapper> <LayoutWrapper>{children}</LayoutWrapper>
</body> </body>
</html> </html>

View file

@ -1,117 +0,0 @@
import { Post, PrismaClient, File, User } from "@prisma/client"
const prisma = new PrismaClient()
export default prisma
export type { User, AuthTokens, File, Post } 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<PostWithFiles | undefined> => {
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
}
}
export async function getPostsByUser(userId: string): Promise<Post[]>
export async function getPostsByUser(
userId: string,
includeFiles: true
): Promise<PostWithFiles[]>
export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
const sharedOptions = {
take: 20,
orderBy: {
createdAt: "desc" as const
}
}
const posts = await prisma.post.findMany({
where: {
authorId: userId
},
...sharedOptions
})
if (withFiles) {
return Promise.all(
posts.map(async (post) => {
const files = await prisma.file.findMany({
where: {
postId: post.id
},
...sharedOptions
})
return {
...post,
files
}
})
)
}
return posts
}
export const getUserById = async (userId: User["id"]) => {
const user = await prisma.user.findUnique({
where: {
id: userId
},
select: {
id: true,
email: true,
displayName: true,
role: true,
username: true
}
})
return user
}
export const isUserAdmin = async (userId: User["id"]) => {
const user = await prisma.user.findUnique({
where: {
id: userId
},
select: {
role: true
}
})
return user?.role?.toLowerCase() === "admin"
}

View file

@ -1,12 +1,14 @@
"use client" "use client"
import Header from "@components/header"
import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist" import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist"
import { ThemeProvider } from "next-themes" import { ThemeProvider } from "next-themes"
import { SkeletonTheme } from "react-loading-skeleton" import { SkeletonTheme } from "react-loading-skeleton"
import styles from "@styles/Home.module.css"
export function LayoutWrapper({ children }: { children: React.ReactNode }) { export function LayoutWrapper({
children,
}: {
children: React.ReactNode
}) {
const skeletonBaseColor = "var(--light-gray)" const skeletonBaseColor = "var(--light-gray)"
const skeletonHighlightColor = "var(--lighter-gray)" const skeletonHighlightColor = "var(--lighter-gray)"
@ -30,7 +32,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
dropdownBoxShadow: "0 0 0 1px var(--lighter-gray)", dropdownBoxShadow: "0 0 0 1px var(--lighter-gray)",
shadowSmall: "0 0 0 1px var(--lighter-gray)", shadowSmall: "0 0 0 1px var(--lighter-gray)",
shadowLarge: "0 0 0 1px var(--lighter-gray)", shadowLarge: "0 0 0 1px var(--lighter-gray)",
shadowMedium: "0 0 0 1px var(--lighter-gray)", shadowMedium: "0 0 0 1px var(--lighter-gray)"
}, },
layout: { layout: {
gap: "var(--gap)", gap: "var(--gap)",
@ -59,12 +61,11 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) {
attribute="data-theme" attribute="data-theme"
> >
<CssBaseline /> <CssBaseline />
<Page width={"100%"}> <Page width={"100%"} style={{
<Page.Header> marginTop: "0 !important",
<Header /> paddingTop: "0 !important"
</Page.Header> }}>
{children}
<Page.Content className={styles.main}>{children}</Page.Content>
</Page> </Page>
</ThemeProvider> </ThemeProvider>
</SkeletonTheme> </SkeletonTheme>

View file

@ -14,6 +14,8 @@
flex-direction: column; flex-direction: column;
place-items: center; place-items: center;
gap: 10px; gap: 10px;
max-width: 300px;
width: 100%;
} }
.formContentSpace { .formContentSpace {

View file

@ -4,11 +4,9 @@ import { FormEvent, useState } from "react"
import styles from "./auth.module.css" import styles from "./auth.module.css"
import { useRouter } from "next/navigation" import { useRouter } from "next/navigation"
import Link from "../link" import Link from "../link"
import useSignedIn from "@lib/hooks/use-signed-in"
import { USER_COOKIE_NAME } from "@lib/constants"
import { setCookie } from "cookies-next"
import { Button, Input, Note } from "@geist-ui/core/dist" import { Button, Input, Note } from "@geist-ui/core/dist"
import { signIn } from "next-auth/react"
import { Github as GithubIcon } from "@geist-ui/icons"
const NO_EMPTY_SPACE_REGEX = /^\S*$/ const NO_EMPTY_SPACE_REGEX = /^\S*$/
const ERROR_MESSAGE = const ERROR_MESSAGE =
"Provide a non empty username and a password with at least 6 characters" "Provide a non empty username and a password with at least 6 characters"
@ -27,29 +25,27 @@ const Auth = ({
const [serverPassword, setServerPassword] = useState("") const [serverPassword, setServerPassword] = useState("")
const [errorMsg, setErrorMsg] = useState("") const [errorMsg, setErrorMsg] = useState("")
const signingIn = page === "signin" const signingIn = page === "signin"
const { signin } = useSignedIn()
const handleJson = (json: any) => { const handleJson = (json: any) => {
signin(json.token) // setCookie(USER_COOKIE_NAME, json.userId)
setCookie(USER_COOKIE_NAME, json.userId)
router.push("/new") router.push("/new")
} }
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => { const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault() e.preventDefault()
if ( // if (
!signingIn && // !signingIn &&
(!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) // (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6)
) // )
return setErrorMsg(ERROR_MESSAGE) // return setErrorMsg(ERROR_MESSAGE)
if ( // if (
!signingIn && // !signingIn &&
requiresServerPassword && // requiresServerPassword &&
!NO_EMPTY_SPACE_REGEX.test(serverPassword) // !NO_EMPTY_SPACE_REGEX.test(serverPassword)
) // )
return setErrorMsg(ERROR_MESSAGE) // return setErrorMsg(ERROR_MESSAGE)
else setErrorMsg("") // else setErrorMsg("")
const reqOpts = { const reqOpts = {
method: "POST", method: "POST",
@ -60,12 +56,18 @@ const Auth = ({
} }
try { try {
const signUrl = signingIn ? "/api/auth/signin" : "/api/auth/signup" // signIn("credentials", {
const resp = await fetch(signUrl, reqOpts) // callbackUrl: "/new",
const json = await resp.json() // redirect: false,
if (!resp.ok) throw new Error(json.error.message) // username,
// password,
handleJson(json) // serverPassword
// })
// 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)
// handleJson(json)
} catch (err: any) { } catch (err: any) {
setErrorMsg(err.message ?? "Something went wrong") setErrorMsg(err.message ?? "Something went wrong")
} }
@ -77,9 +79,10 @@ const Auth = ({
<div className={styles.formContentSpace}> <div className={styles.formContentSpace}>
<h1>{signingIn ? "Sign In" : "Sign Up"}</h1> <h1>{signingIn ? "Sign In" : "Sign Up"}</h1>
</div> </div>
<form onSubmit={handleSubmit}> {/* <form onSubmit={handleSubmit}> */}
<form>
<div className={styles.formGroup}> <div className={styles.formGroup}>
<Input {/* <Input
htmlType="text" htmlType="text"
id="username" id="username"
value={username} value={username}
@ -87,6 +90,7 @@ const Auth = ({
placeholder="Username" placeholder="Username"
required required
minLength={3} minLength={3}
width="100%"
/> />
<Input <Input
htmlType="password" htmlType="password"
@ -96,7 +100,9 @@ const Auth = ({
placeholder="Password" placeholder="Password"
required required
minLength={6} minLength={6}
/> width="100%"
/> */}
{/* sign in with github */}
{requiresServerPassword && ( {requiresServerPassword && (
<Input <Input
htmlType="password" htmlType="password"
@ -110,10 +116,20 @@ const Auth = ({
width="100%" width="100%"
/> />
)} )}
<Button
<Button width={"100%"} htmlType="submit"> htmlType="submit"
{signingIn ? "Sign In" : "Sign Up"} type="success-light"
auto
width="100%"
icon={<GithubIcon />}
onClick={() => signIn("github")}
>
Sign in with GitHub
</Button> </Button>
{/* <Button width={"100%"} htmlType="submit">
{signingIn ? "Sign In" : "Sign Up"}
</Button> */}
</div> </div>
<div className={styles.formContentSpace}> <div className={styles.formContentSpace}>
{signingIn ? ( {signingIn ? (
@ -125,7 +141,7 @@ const Auth = ({
</p> </p>
) : ( ) : (
<p> <p>
Already have an account?{" "} Have an account?{" "}
<Link colored href="/signin"> <Link colored href="/signin">
Sign in Sign in
</Link> </Link>

View file

@ -23,7 +23,7 @@
.mobile { .mobile {
position: absolute; position: absolute;
z-index: 1; z-index: 1000;
} }
.controls { .controls {

View file

@ -11,7 +11,6 @@ import {
import { useCallback, useEffect, useMemo, useState } from "react" import { useCallback, useEffect, useMemo, useState } from "react"
import styles from "./header.module.css" import styles from "./header.module.css"
import useSignedIn from "../../lib/hooks/use-signed-in"
import HomeIcon from "@geist-ui/icons/home" import HomeIcon from "@geist-ui/icons/home"
import MenuIcon from "@geist-ui/icons/menu" import MenuIcon from "@geist-ui/icons/menu"
@ -25,7 +24,7 @@ import MoonIcon from "@geist-ui/icons/moon"
import SettingsIcon from "@geist-ui/icons/settings" import SettingsIcon from "@geist-ui/icons/settings"
import SunIcon from "@geist-ui/icons/sun" import SunIcon from "@geist-ui/icons/sun"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import useUserData from "@lib/hooks/use-user-data" // import useUserData from "@lib/hooks/use-user-data"
import Link from "next/link" import Link from "next/link"
import { usePathname } from "next/navigation" import { usePathname } from "next/navigation"
@ -37,13 +36,13 @@ type Tab = {
href?: string href?: string
} }
const Header = () => { const Header = ({ signedIn = false }) => {
const pathname = usePathname() const pathname = usePathname()
const [expanded, setExpanded] = useState<boolean>(false) const [expanded, setExpanded] = useState<boolean>(false)
const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true }) const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true })
const isMobile = useMediaQuery("xs", { match: "down" }) const isMobile = useMediaQuery("xs", { match: "down" })
const { signedIn: isSignedIn } = useSignedIn() // const { status } = useSession()
const userData = useUserData() // const signedIn = status === "authenticated"
const [pages, setPages] = useState<Tab[]>([]) const [pages, setPages] = useState<Tab[]>([])
const { setTheme, resolvedTheme } = useTheme() const { setTheme, resolvedTheme } = useTheme()
@ -76,7 +75,7 @@ const Header = () => {
} }
] ]
if (isSignedIn) if (signedIn)
setPages([ setPages([
{ {
name: "new", name: "new",
@ -126,20 +125,20 @@ const Header = () => {
}, },
...defaultPages ...defaultPages
]) ])
if (userData?.role === "admin") { // if (userData?.role === "admin") {
setPages((pages) => [ // setPages((pages) => [
...pages, // ...pages,
{ // {
name: "admin", // name: "admin",
icon: <SettingsIcon />, // icon: <SettingsIcon />,
value: "admin", // value: "admin",
href: "/admin" // href: "/admin"
} // }
]) // ])
} // }
// TODO: investigate deps causing infinite loop // TODO: investigate deps causing infinite loop
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMobile, isSignedIn, resolvedTheme, userData]) }, [isMobile, resolvedTheme])
const onTabChange = useCallback( const onTabChange = useCallback(
(tab: string) => { (tab: string) => {
@ -172,7 +171,6 @@ const Header = () => {
return ( return (
<Link key={tab.value} href={tab.href} className={styles.tab}> <Link key={tab.value} href={tab.href} className={styles.tab}>
<Button <Button
className={activeStyle}
auto={isMobile ? false : true} auto={isMobile ? false : true}
icon={tab.icon} icon={tab.icon}
shadow={false} shadow={false}

View file

@ -14,7 +14,7 @@ import { ChangeEvent } from "react"
import DatePicker from "react-datepicker" import DatePicker from "react-datepicker"
import getTitleForPostCopy from "@lib/get-title-for-post-copy" import getTitleForPostCopy from "@lib/get-title-for-post-copy"
import Description from "./description" import Description from "./description"
import { PostWithFiles } from "app/prisma" import { PostWithFiles } from "@lib/server/prisma"
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants" import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next"

View file

@ -2,7 +2,7 @@ import React from "react"
type PageSeoProps = { type PageSeoProps = {
title?: string title?: string
description?: string description?: string | null
isLoading?: boolean isLoading?: boolean
isPrivate?: boolean isPrivate?: boolean
} }

View file

@ -10,7 +10,7 @@ import useDebounce from "@lib/hooks/use-debounce"
import Link from "@components/link" import Link from "@components/link"
import { TOKEN_COOKIE_NAME } from "@lib/constants" import { TOKEN_COOKIE_NAME } from "@lib/constants"
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next"
import type { PostWithFiles } from "app/prisma" import type { PostWithFiles } from "@lib/server/prisma"
type Props = { type Props = {
initialPosts: PostWithFiles[] initialPosts: PostWithFiles[]

View file

@ -17,7 +17,7 @@ import { useRouter } from "next/router"
import Parent from "@geist-ui/icons/arrowUpCircle" import Parent from "@geist-ui/icons/arrowUpCircle"
import styles from "./list-item.module.css" import styles from "./list-item.module.css"
import Link from "@components/link" import Link from "@components/link"
import { PostWithFiles, File } from "app/prisma" import { PostWithFiles, File } from "@lib/server/prisma"
import { PostVisibility } from "@lib/types" import { PostVisibility } from "@lib/types"
// TODO: isOwner should default to false so this can be used generically // TODO: isOwner should default to false so this can be used generically

View file

@ -102,12 +102,8 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
const isAvailable = !isExpired && !isProtected && post.title const isAvailable = !isExpired && !isProtected && post.title
return ( return (
<Page width={"100%"}> <>
<PageSeo
title={`${post.title} - Drift`}
description={post.description}
isPrivate={false}
/>
{!isAvailable && <PasswordModalPage setPost={setPost} />} {!isAvailable && <PasswordModalPage setPost={setPost} />}
<Page.Content className={homeStyles.main}> <Page.Content className={homeStyles.main}>
<div className={styles.header}> <div className={styles.header}>
@ -178,7 +174,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
)} )}
<ScrollToTop /> <ScrollToTop />
</Page.Content> </Page.Content>
</Page> </>
) )
} }

View file

@ -1,3 +1,4 @@
'use client';
import { Fieldset, Text, Divider } from "@geist-ui/core/dist" import { Fieldset, Text, Divider } from "@geist-ui/core/dist"
import styles from "./settings-group.module.css" import styles from "./settings-group.module.css"

View file

@ -1,28 +0,0 @@
"use client"
import Password from "./sections/password"
import Profile from "./sections/profile"
import SettingsGroup from "../settings-group"
const SettingsPage = () => {
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--gap)",
marginBottom: "var(--gap)"
}}
>
<h1>Settings</h1>
<SettingsGroup title="Profile">
<Profile />
</SettingsGroup>
<SettingsGroup title="Password">
<Password />
</SettingsGroup>
</div>
)
}
export default SettingsPage

View file

@ -2,21 +2,20 @@
import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist" import { Note, Input, Textarea, Button, useToasts } from "@geist-ui/core/dist"
import { TOKEN_COOKIE_NAME } from "@lib/constants" import { TOKEN_COOKIE_NAME } from "@lib/constants"
import useUserData from "@lib/hooks/use-user-data"
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next"
import { User } from "next-auth"
import { useEffect, useState } from "react" import { useEffect, useState } from "react"
const Profile = () => { const Profile = ({ user }: { user: User }) => {
const user = useUserData()
const [name, setName] = useState<string>() const [name, setName] = useState<string>()
const [email, setEmail] = useState<string>() const [email, setEmail] = useState<string>()
const [bio, setBio] = useState<string>() const [bio, setBio] = useState<string>()
useEffect(() => { useEffect(() => {
console.log(user) console.log(user)
if (user?.displayName) setName(user.displayName) // if (user?.displayName) setName(user.displayName)
if (user?.email) setEmail(user.email) if (user?.email) setEmail(user.email)
if (user?.bio) setBio(user.bio) // if (user?.bio) setBio(user.bio)
}, [user]) }, [user])
const { setToast } = useToasts() const { setToast } = useToasts()

View file

@ -10,6 +10,8 @@ type Config = {
welcome_content: string welcome_content: string
welcome_title: string welcome_title: string
url: string url: string
GITHUB_CLIENT_ID: string
GITHUB_CLIENT_SECRET: string
} }
type EnvironmentValue = string | undefined type EnvironmentValue = string | undefined
@ -80,7 +82,9 @@ export const config = (env: Environment): Config => {
registration_password: env.REGISTRATION_PASSWORD ?? "", registration_password: env.REGISTRATION_PASSWORD ?? "",
welcome_content: env.WELCOME_CONTENT ?? "", welcome_content: env.WELCOME_CONTENT ?? "",
welcome_title: env.WELCOME_TITLE ?? "", welcome_title: env.WELCOME_TITLE ?? "",
url: "http://localhost:3000" url: "http://localhost:3000",
GITHUB_CLIENT_ID: env.GITHUB_CLIENT_ID ?? "",
GITHUB_CLIENT_SECRET: env.GITHUB_CLIENT_SECRET ?? "",
} }
return config return config
} }

View file

@ -17,13 +17,13 @@ const useSignedIn = () => {
setCookie(TOKEN_COOKIE_NAME, token) setCookie(TOKEN_COOKIE_NAME, token)
} }
useEffect(() => { // useEffect(() => {
if (token) { // if (token) {
setSignedIn(true) // setSignedIn(true)
} else { // } else {
setSignedIn(false) // setSignedIn(false)
} // }
}, [setSignedIn, token]) // }, [setSignedIn, token])
console.log("signed in", signedIn) console.log("signed in", signedIn)

View file

@ -1,46 +0,0 @@
import { TOKEN_COOKIE_NAME } from "@lib/constants"
import { User } from "@lib/types"
import { deleteCookie, getCookie } from "cookies-next"
import { useRouter } from "next/navigation"
import { useEffect, useState } from "react"
const useUserData = () => {
const cookie = getCookie(TOKEN_COOKIE_NAME)
const [authToken, setAuthToken] = useState<string>(
cookie ? String(cookie) : ""
)
const [user, setUser] = useState<User>()
const router = useRouter()
useEffect(() => {
const token = getCookie(TOKEN_COOKIE_NAME)
if (token) {
setAuthToken(String(token))
}
}, [setAuthToken])
useEffect(() => {
if (authToken) {
const fetchUser = async () => {
const response = await fetch(`/api/user/self`, {
headers: {
Authorization: `Bearer ${authToken}`
}
})
if (response.ok) {
const user = await response.json()
setUser(user)
} else {
// deleteCookie("drift-token")
// setAuthToken("")
// router.push("/")
console.log("not ok")
}
}
fetchUser()
}
}, [authToken, router])
return user
}
export default useUserData

61
client/lib/server/auth.ts Normal file
View file

@ -0,0 +1,61 @@
import { PrismaAdapter } from "@next-auth/prisma-adapter"
import { NextAuthOptions } from "next-auth"
import GitHubProvider from "next-auth/providers/github"
import prisma from "lib/server/prisma"
import config from "@lib/config"
const providers: NextAuthOptions["providers"] = [
GitHubProvider({
clientId: config.GITHUB_CLIENT_ID,
clientSecret: config.GITHUB_CLIENT_SECRET
}),
]
export const authOptions: NextAuthOptions = {
// see https://github.com/prisma/prisma/issues/16117 / https://github.com/shadcn/taxonomy
adapter: PrismaAdapter(prisma as any),
session: {
strategy: "jwt"
},
pages: {
signIn: "/signin"
},
providers,
callbacks: {
async session({ token, session }) {
if (token) {
session.user.id = token.id
session.user.name = token.name
session.user.email = token.email
session.user.image = token.picture
session.user.role = token.role
}
return session
},
async jwt({ token, user }) {
const dbUser = await prisma.user.findFirst({
where: {
email: token.email
}
})
if (!dbUser) {
// TODO: user should be defined?
if (user) {
token.id = user.id
}
return token
}
return {
id: dbUser.id,
name: dbUser.username,
email: dbUser.email,
picture: dbUser.image,
role: dbUser.role
}
}
}
} as const

View file

@ -1,6 +1,6 @@
import config from "@lib/config" import config from "@lib/config"
import { User } from "@prisma/client" import { User } from "@prisma/client"
import prisma from "app/prisma" import prisma from "@lib/server/prisma"
import { sign } from "jsonwebtoken" import { sign } from "jsonwebtoken"
export async function generateAndExpireAccessToken(userId: User["id"]) { export async function generateAndExpireAccessToken(userId: User["id"]) {

View file

@ -1,5 +1,5 @@
import markdown from "../render-markdown" import markdown from "../render-markdown"
import type { File } from "app/prisma" import type { File } from "@lib/server/prisma"
/** /**
* returns rendered HTML from a Drift file * returns rendered HTML from a Drift file
*/ */

View file

@ -1,12 +0,0 @@
import { useRouter } from "next/navigation"
import { isSignedIn } from "../is-signed-in"
export const useRedirectIfNotAuthed = (to = "/signin") => {
const router = useRouter()
const signedIn = isSignedIn()
if (!signedIn) {
router.push(to)
}
}

View file

@ -1,7 +0,0 @@
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
import { cookies } from "next/headers"
export const isSignedIn = () => {
const cookieList = cookies()
return cookieList.has(TOKEN_COOKIE_NAME) && cookieList.has(USER_COOKIE_NAME)
}

View file

@ -2,7 +2,7 @@
import config from "@lib/config" import config from "@lib/config"
import { User } from "@prisma/client" import { User } from "@prisma/client"
import prisma from "app/prisma" import prisma from "@lib/server/prisma"
import * as jwt from "jsonwebtoken" import * as jwt from "jsonwebtoken"
import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next" import next, { NextApiHandler, NextApiRequest, NextApiResponse } from "next"

173
client/lib/server/prisma.ts Normal file
View file

@ -0,0 +1,173 @@
declare global {
var prisma: PrismaClient | undefined
}
import config from "@lib/config"
import { Post, PrismaClient, File, User } from "@prisma/client"
import { generateAndExpireAccessToken } from "./generate-access-token"
const prisma = new PrismaClient()
export default prisma
// https://next-auth.js.org/adapters/prisma
const client = globalThis.prisma || prisma
if (process.env.NODE_ENV !== "production") globalThis.prisma = client
export type { User, AuthTokens, File, Post } 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
}
/**
* When passed in a postId, fetches the post and then the files.
* If passed a Post, it will fetch the files
* @param postIdOrPost Post or postId
* @returns Promise<PostWithFiles>
*/
export async function getPostWithFiles(postId: string): Promise<PostWithFiles>
export async function getPostWithFiles(postObject: Post): Promise<PostWithFiles>
export async function getPostWithFiles(
postIdOrObject: string | Post
): Promise<PostWithFiles | undefined> {
let post: Post | null
if (typeof postIdOrObject === "string") {
post = await prisma.post.findUnique({
where: {
id: postIdOrObject
}
})
} else {
post = postIdOrObject
}
if (!post) {
return undefined
}
const files = await getFilesForPost(post.id)
if (!files) {
return undefined
}
return {
...post,
files
}
}
export async function getPostsByUser(userId: string): Promise<Post[]>
export async function getPostsByUser(
userId: string,
includeFiles: true
): Promise<PostWithFiles[]>
export async function getPostsByUser(userId: User["id"], withFiles?: boolean) {
const posts = await prisma.post.findMany({
where: {
authorId: userId
}
})
if (withFiles) {
const postsWithFiles = await Promise.all(
posts.map(async (post) => {
const files = await getPostWithFiles(post)
return {
...post,
files
}
})
)
return postsWithFiles
}
return posts
}
export const getUserById = async (userId: User["id"]) => {
const user = await prisma.user.findUnique({
where: {
id: userId
},
select: {
id: true,
email: true,
// displayName: true,
role: true,
username: true
}
})
return user
}
export const isUserAdmin = async (userId: User["id"]) => {
const user = await prisma.user.findUnique({
where: {
id: userId
},
select: {
role: true
}
})
return user?.role?.toLowerCase() === "admin"
}
export const createUser = async (username: string, password: string, serverPassword?: string) => {
if (!username || !password) {
throw new Error("Missing param")
}
if (
config.registration_password &&
serverPassword !== config.registration_password
) {
console.log("Registration password mismatch")
throw new Error("Wrong registration password")
}
// 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 generateAndExpireAccessToken(user.id)
return {
// user,
// token
}
}
export const getPostById = async (postId: Post["id"]) => {
const post = await prisma.post.findUnique({
where: {
id: postId
}
})
return post
}

View file

@ -0,0 +1,15 @@
import 'server-only';
import { unstable_getServerSession } from "next-auth/next"
import { authOptions } from "./auth"
export async function getSession() {
return await unstable_getServerSession(authOptions)
}
export async function getCurrentUser() {
const session = await getSession()
return session?.user
}

View file

@ -1,5 +1,5 @@
import { USER_COOKIE_NAME, TOKEN_COOKIE_NAME } from "@lib/constants" import { USER_COOKIE_NAME, TOKEN_COOKIE_NAME } from "@lib/constants"
import { User } from "app/prisma" import { User } from "@lib/server/prisma"
import { setCookie } from "cookies-next" import { setCookie } from "cookies-next"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { generateAndExpireAccessToken } from "./generate-access-token" import { generateAndExpireAccessToken } from "./generate-access-token"

View file

@ -1,69 +1,39 @@
import { NextFetchEvent, NextResponse } from "next/server" import { getToken } from "next-auth/jwt"
import type { NextRequest } from "next/server" import { withAuth } from "next-auth/middleware"
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants" import { NextResponse } from "next/server"
import serverConfig from "@lib/config"
const PUBLIC_FILE = /\.(.*)$/ export default withAuth(
async function middleware(req) {
const token = await getToken({ req })
const isAuth = !!token
const isAuthPage =
req.nextUrl.pathname.startsWith("/signup") ||
req.nextUrl.pathname.startsWith("/signin")
export function middleware(req: NextRequest, event: NextFetchEvent) { if (isAuthPage) {
const pathname = req.nextUrl.pathname if (isAuth) {
const signedIn = req.cookies.get(TOKEN_COOKIE_NAME) return NextResponse.redirect(new URL("/new", req.url))
const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href
const isPageRequest =
!PUBLIC_FILE.test(pathname) &&
// header added when next/link pre-fetches a route
!req.headers.get("x-middleware-preflight")
if (!req.headers.get("x-middleware-preflight") && pathname === "/signout") {
// If you're signed in we remove the cookie and redirect to the home page
// If you're not signed in we redirect to the home page
if (signedIn) {
const resp = NextResponse.redirect(getURL(""))
resp.cookies.delete(TOKEN_COOKIE_NAME)
resp.cookies.delete(USER_COOKIE_NAME)
const signoutPromise = new Promise((resolve) => {
fetch(`${serverConfig.url}/auth/signout`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${signedIn}`,
"x-secret-key": process.env.SECRET_KEY || ""
}
}).then(() => {
resolve(true)
})
})
event.waitUntil(signoutPromise)
return resp
}
} else if (isPageRequest) {
// if (signedIn) {
// if (
// pathname === "/" ||
// pathname === "/signin" ||
// pathname === "/signup"
// ) {
// return NextResponse.redirect(getURL("new"))
// }
// } else if (!signedIn) {
// if (pathname.startsWith("/new")) {
// return NextResponse.redirect(getURL("signin"))
// }
// }
if (pathname.includes("/protected/") || pathname.includes("/private/")) {
const urlWithoutVisibility = pathname
.replace("/protected/", "/")
.replace("/private/", "/")
.substring(1)
return NextResponse.redirect(getURL(urlWithoutVisibility))
}
} }
return NextResponse.next() return null
} }
if (!isAuth) {
return NextResponse.redirect(new URL("/signin", req.url))
}
},
{
callbacks: {
async authorized() {
// This is a work-around for handling redirect on auth pages.
// We return true here so that the middleware function above
// is always called.
return true
}
}
}
)
export const config = { export const config = {
match: [ match: [
// "/signout", // "/signout",
@ -71,9 +41,6 @@ export const config = {
"/signin", "/signin",
"/signup", "/signup",
"/new", "/new",
"/protected/:path*",
"/private/:path*" "/private/:path*"
] ]
} }

View file

@ -14,7 +14,8 @@
"dependencies": { "dependencies": {
"@geist-ui/core": "^2.3.8", "@geist-ui/core": "^2.3.8",
"@geist-ui/icons": "1.0.2", "@geist-ui/icons": "1.0.2",
"@prisma/client": "^4.6.0", "@next-auth/prisma-adapter": "^1.0.5",
"@prisma/client": "^4.6.1",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"client-zip": "2.2.1", "client-zip": "2.2.1",
"clsx": "^1.2.1", "clsx": "^1.2.1",
@ -22,7 +23,8 @@
"dotenv": "16.0.0", "dotenv": "16.0.0",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^8.5.1",
"marked": "^4.2.2", "marked": "^4.2.2",
"next": "13.0.3-canary.2", "next": "13.0.3-canary.4",
"next-auth": "^4.16.4",
"next-themes": "npm:@wits/next-themes@0.2.7", "next-themes": "npm:@wits/next-themes@0.2.7",
"prism-react-renderer": "^1.3.5", "prism-react-renderer": "^1.3.5",
"rc-table": "7.24.1", "rc-table": "7.24.1",
@ -32,6 +34,7 @@
"react-dropzone": "14.2.3", "react-dropzone": "14.2.3",
"react-hot-toast": "^2.4.0", "react-hot-toast": "^2.4.0",
"react-loading-skeleton": "3.1.0", "react-loading-skeleton": "3.1.0",
"server-only": "^0.0.1",
"showdown": "^2.1.0", "showdown": "^2.1.0",
"swr": "1.3.0", "swr": "1.3.0",
"textarea-markdown-editor": "0.1.13", "textarea-markdown-editor": "0.1.13",
@ -52,7 +55,7 @@
"eslint-config-next": "13.0.2", "eslint-config-next": "13.0.2",
"next-unused": "0.0.6", "next-unused": "0.0.6",
"prettier": "2.6.2", "prettier": "2.6.2",
"prisma": "^4.6.0", "prisma": "^4.6.1",
"typescript": "4.6.4", "typescript": "4.6.4",
"typescript-plugin-css-modules": "3.4.0" "typescript-plugin-css-modules": "3.4.0"
}, },

View file

@ -0,0 +1,4 @@
import { authOptions } from "@lib/server/auth"
import NextAuth from "next-auth"
export default NextAuth(authOptions)

View file

@ -1,7 +1,9 @@
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import prisma from "app/prisma" import prisma from "@lib/server/prisma"
import bcrypt from "bcrypt" import bcrypt from "bcrypt"
import { signin } from "@lib/server/signin" import { signin } from "@lib/server/signin"
import { setCookie } from "cookies-next"
import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants"
export default async function handler( export default async function handler(
req: NextApiRequest, req: NextApiRequest,
@ -18,7 +20,7 @@ export default async function handler(
} }
}) })
if (!user) { if (!user || !user.password) {
return res.status(401).json({ error: "Unauthorized" }) return res.status(401).json({ error: "Unauthorized" })
} }
@ -27,7 +29,23 @@ export default async function handler(
return res.status(401).json({ error: "Unauthorized" }) return res.status(401).json({ error: "Unauthorized" })
} }
const token = await signin(user.id, req, res); const token = await signin(user.id, req, res)
setCookie(TOKEN_COOKIE_NAME, token, {
path: "/",
maxAge: 60 * 60 * 24 * 7, // 1 week
httpOnly: true,
secure: process.env.NODE_ENV === "production",
req,
res
})
setCookie(USER_COOKIE_NAME, user.id, {
path: "/",
maxAge: 60 * 60 * 24 * 7, // 1 week
httpOnly: true,
secure: process.env.NODE_ENV === "production",
req,
res
})
return res.status(201).json({ token: token, userId: user.id }) return res.status(201).json({ token: token, userId: user.id })
} }

View file

@ -0,0 +1,12 @@
import { NextApiRequest, NextApiResponse } from "next"
import { createUser } from "@lib/server/prisma"
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const { username, password, serverPassword } = req.body
const { user, token } = await createUser(username, password, serverPassword)
return res.status(201).json({ token: token, userId: user.id })
}

View file

@ -1,45 +0,0 @@
import config from "@lib/config"
import { NextApiRequest, NextApiResponse } from "next"
import prisma from "app/prisma"
import bcrypt, { genSalt } from "bcrypt"
import { generateAndExpireAccessToken } from "@lib/server/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 generateAndExpireAccessToken(user.id)
return res.status(201).json({ token: token, userId: user.id })
}

View file

@ -1,6 +1,6 @@
import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file" import { getHtmlFromFile } from "@lib/server/get-html-from-drift-file"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import prisma from "app/prisma" import prisma from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
export default async function handler( export default async function handler(

View file

@ -1,5 +1,5 @@
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import { getPostsByUser } from "app/prisma" import { getPostsByUser } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
export default async function handle( export default async function handle(

View file

@ -5,7 +5,7 @@
// }) // })
import { USER_COOKIE_NAME } from "@lib/constants" import { USER_COOKIE_NAME } from "@lib/constants"
import prisma, { getUserById } from "app/prisma" import prisma, { getUserById } from "@lib/server/prisma"
import { getCookie } from "cookies-next" import { getCookie } from "cookies-next"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"

View file

@ -3,8 +3,9 @@ lockfileVersion: 5.4
specifiers: specifiers:
'@geist-ui/core': ^2.3.8 '@geist-ui/core': ^2.3.8
'@geist-ui/icons': 1.0.2 '@geist-ui/icons': 1.0.2
'@next-auth/prisma-adapter': ^1.0.5
'@next/bundle-analyzer': 12.1.6 '@next/bundle-analyzer': 12.1.6
'@prisma/client': ^4.6.0 '@prisma/client': ^4.6.1
'@types/bcrypt': ^5.0.0 '@types/bcrypt': ^5.0.0
'@types/jsonwebtoken': ^8.5.9 '@types/jsonwebtoken': ^8.5.9
'@types/marked': ^4.0.7 '@types/marked': ^4.0.7
@ -23,12 +24,13 @@ specifiers:
eslint-config-next: 13.0.2 eslint-config-next: 13.0.2
jsonwebtoken: ^8.5.1 jsonwebtoken: ^8.5.1
marked: ^4.2.2 marked: ^4.2.2
next: 13.0.3-canary.2 next: 13.0.3-canary.4
next-auth: ^4.16.4
next-themes: npm:@wits/next-themes@0.2.7 next-themes: npm:@wits/next-themes@0.2.7
next-unused: 0.0.6 next-unused: 0.0.6
prettier: 2.6.2 prettier: 2.6.2
prism-react-renderer: ^1.3.5 prism-react-renderer: ^1.3.5
prisma: ^4.6.0 prisma: ^4.6.1
rc-table: 7.24.1 rc-table: 7.24.1
react: 18.2.0 react: 18.2.0
react-datepicker: 4.8.0 react-datepicker: 4.8.0
@ -36,6 +38,7 @@ specifiers:
react-dropzone: 14.2.3 react-dropzone: 14.2.3
react-hot-toast: ^2.4.0 react-hot-toast: ^2.4.0
react-loading-skeleton: 3.1.0 react-loading-skeleton: 3.1.0
server-only: ^0.0.1
sharp: ^0.31.2 sharp: ^0.31.2
showdown: ^2.1.0 showdown: ^2.1.0
swr: 1.3.0 swr: 1.3.0
@ -47,7 +50,8 @@ specifiers:
dependencies: dependencies:
'@geist-ui/core': 2.3.8_biqbaboplfbrettd7655fr4n2y '@geist-ui/core': 2.3.8_biqbaboplfbrettd7655fr4n2y
'@geist-ui/icons': 1.0.2_zhza2kbnl2wkkf7vqdl3ton2f4 '@geist-ui/icons': 1.0.2_zhza2kbnl2wkkf7vqdl3ton2f4
'@prisma/client': 4.6.0_prisma@4.6.0 '@next-auth/prisma-adapter': 1.0.5_2pl3b2nwmjya7el2zbe6cwkney
'@prisma/client': 4.6.1_prisma@4.6.1
bcrypt: 5.1.0 bcrypt: 5.1.0
client-zip: 2.2.1 client-zip: 2.2.1
clsx: 1.2.1 clsx: 1.2.1
@ -55,8 +59,9 @@ dependencies:
dotenv: 16.0.0 dotenv: 16.0.0
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
marked: 4.2.2 marked: 4.2.2
next: 13.0.3-canary.2_biqbaboplfbrettd7655fr4n2y next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
next-themes: /@wits/next-themes/0.2.7_qjr36eup74ongf7bl2iopfchwe next-auth: 4.16.4_hsmqkug4agizydugca45idewda
next-themes: /@wits/next-themes/0.2.7_hsmqkug4agizydugca45idewda
prism-react-renderer: 1.3.5_react@18.2.0 prism-react-renderer: 1.3.5_react@18.2.0
rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y rc-table: 7.24.1_biqbaboplfbrettd7655fr4n2y
react: 18.2.0 react: 18.2.0
@ -65,6 +70,7 @@ dependencies:
react-dropzone: 14.2.3_react@18.2.0 react-dropzone: 14.2.3_react@18.2.0
react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y react-hot-toast: 2.4.0_biqbaboplfbrettd7655fr4n2y
react-loading-skeleton: 3.1.0_react@18.2.0 react-loading-skeleton: 3.1.0_react@18.2.0
server-only: 0.0.1
showdown: 2.1.0 showdown: 2.1.0
swr: 1.3.0_react@18.2.0 swr: 1.3.0_react@18.2.0
textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y textarea-markdown-editor: 0.1.13_biqbaboplfbrettd7655fr4n2y
@ -88,7 +94,7 @@ devDependencies:
eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i eslint-config-next: 13.0.2_hsmo2rtalirsvadpuxki35bq2i
next-unused: 0.0.6 next-unused: 0.0.6
prettier: 2.6.2 prettier: 2.6.2
prisma: 4.6.0 prisma: 4.6.1
typescript: 4.6.4 typescript: 4.6.4
typescript-plugin-css-modules: 3.4.0_typescript@4.6.4 typescript-plugin-css-modules: 3.4.0_typescript@4.6.4
@ -211,6 +217,16 @@ packages:
- supports-color - supports-color
dev: false dev: false
/@next-auth/prisma-adapter/1.0.5_2pl3b2nwmjya7el2zbe6cwkney:
resolution: {integrity: sha512-VqMS11IxPXrPGXw6Oul6jcyS/n8GLOWzRMrPr3EMdtD6eOalM6zz05j08PcNiis8QzkfuYnCv49OvufTuaEwYQ==}
peerDependencies:
'@prisma/client': '>=2.26.0 || >=3'
next-auth: ^4
dependencies:
'@prisma/client': 4.6.1_prisma@4.6.1
next-auth: 4.16.4_hsmqkug4agizydugca45idewda
dev: false
/@next/bundle-analyzer/12.1.6: /@next/bundle-analyzer/12.1.6:
resolution: {integrity: sha512-WLydwytAeHoC/neXsiIgK+a6Me12PuSpwopnsZgX5JFNwXQ9MlwPeMGS3aTZkYsv8QmSm0Ns9Yh9FkgLKYaUuQ==} resolution: {integrity: sha512-WLydwytAeHoC/neXsiIgK+a6Me12PuSpwopnsZgX5JFNwXQ9MlwPeMGS3aTZkYsv8QmSm0Ns9Yh9FkgLKYaUuQ==}
dependencies: dependencies:
@ -220,8 +236,8 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@next/env/13.0.3-canary.2: /@next/env/13.0.3-canary.4:
resolution: {integrity: sha512-Ugn4VxB+2Bd1LnWcMbjIwNcVYPoBZ8Yo6j2A3MU99pzeYq+TGtHcYPz0xyIAP3Qp7mrH5gx6PITVz7D22u8p7w==} resolution: {integrity: sha512-IKMYPznB0ttgHa1K7nKbfSMM8kne3G7Am+eNeM11cr+HjPljAzl863Ib9UBk6s7oChTAEVtaoKHbAerW/36tWA==}
dev: false dev: false
/@next/eslint-plugin-next/13.0.2: /@next/eslint-plugin-next/13.0.2:
@ -230,8 +246,8 @@ packages:
glob: 7.1.7 glob: 7.1.7
dev: true dev: true
/@next/swc-android-arm-eabi/13.0.3-canary.2: /@next/swc-android-arm-eabi/13.0.3-canary.4:
resolution: {integrity: sha512-ZZG0C+P4czfq5Zyhdouacb3w73w/iOj4KidWCpWlYfTnxlMinPoEDk04xFg5iR665ePlS2mrBnj2OfhckYcFdQ==} resolution: {integrity: sha512-3CXPHZfP7KGwKlrBv451x3l++q1Jxr/5PESk1TkFednJmw+9F6Tno+2RPYEzE++EWxjuAM8SmwHZxhJ6HorOvA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [android] os: [android]
@ -239,8 +255,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-android-arm64/13.0.3-canary.2: /@next/swc-android-arm64/13.0.3-canary.4:
resolution: {integrity: sha512-0Nw4n6Eox1cCp0d9BJ5GQDgW2+8JxoF5asdOdN0E1a6ayygOfsXN/GP3VWcrpLSrx6K1XUO+lgBbCbaOjvnoxA==} resolution: {integrity: sha512-hjsSok+41ZYDghIXMUrvv1eyDboinpDu5kcd/aQTqiV9ukuoQSQFwPd9i8fXVWKOb8w9rfoSLrPoslZXxbMolw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [android] os: [android]
@ -248,8 +264,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-arm64/13.0.3-canary.2: /@next/swc-darwin-arm64/13.0.3-canary.4:
resolution: {integrity: sha512-TkSQVEEcmCfbzotHNHGWe1PkiZZkKPg4QWylZYv8UDfRUwJwR94aJeriOqlGOTkKQ/6a+ulJrVgs50/5gTTIHg==} resolution: {integrity: sha512-DxpeUXj7UcSidRDH0WjDzFlrycNvCKtQgpjPEzljBs2MGXGisuJ/znFkmqbLYwUi71La0nw91Yuz7IrGDpbhag==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [darwin] os: [darwin]
@ -257,8 +273,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-darwin-x64/13.0.3-canary.2: /@next/swc-darwin-x64/13.0.3-canary.4:
resolution: {integrity: sha512-hGCarEZsaSdOWtJOUJc4Sr3oRzUjlI/G+qlyMkaceSTyYx4Xu2/OmDS1fCWxoltlimiHmlJpLnGGaxUgrZ8dkQ==} resolution: {integrity: sha512-jGdLe9QRpbSMkO+Ttpr8fnl2q/s1cQuBvGKM0nHiIUtwuwnho4BjcYQdcCJbjjH2Vs0KMhayZh9REa+52vdAEA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [darwin] os: [darwin]
@ -266,8 +282,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-freebsd-x64/13.0.3-canary.2: /@next/swc-freebsd-x64/13.0.3-canary.4:
resolution: {integrity: sha512-R7WFI/whtuSB6gxmzgqFzeKbrhuSp3ut0GaQK+kvb7NUnFe9xABUksdxEU8bORjVJaADgDsCsCHSsHGqHHl7Mg==} resolution: {integrity: sha512-9VJCLOkbteSozo8kxrqiFJDntARLIn0Uv4aXdvbAuYhEIVRbnP0uA3z1r6d4g8ycC1Yout6z0m3pkg0MHbKV2w==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [freebsd] os: [freebsd]
@ -275,8 +291,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm-gnueabihf/13.0.3-canary.2: /@next/swc-linux-arm-gnueabihf/13.0.3-canary.4:
resolution: {integrity: sha512-RzYf+MTdP8Rvz/fijlxsTP+1S24ziMtCtzq2Ui8Qjg7VIfD9sEuLmMQJpm0k/FscduQdZILoG+QNhD2oW893Wg==} resolution: {integrity: sha512-SBA6Ja07guZI8KnIpMRN6tDvD6tse70c8d9HPwdkK7JziwIBzNDSuLbuA9WB+9/byM70U8jROBKgMUZAsAbnew==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm] cpu: [arm]
os: [linux] os: [linux]
@ -284,8 +300,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-gnu/13.0.3-canary.2: /@next/swc-linux-arm64-gnu/13.0.3-canary.4:
resolution: {integrity: sha512-8TF9UxIAZuQNf4fkyfZ1LcrqqvRI2Li0V2IO0CiCx4wg6xDqBjMH3IZoRwgY3yJ8UxdrFWf8Ec1q2WBYXcODgQ==} resolution: {integrity: sha512-9hQU3mZtzuLAvqaz/72jM2IWtV3lcLFhWqWGCS8yqUCKjkT2ppd/L/VEVuvatC67H5wzpbAJPnDxjPIl7ryiOA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -293,8 +309,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-arm64-musl/13.0.3-canary.2: /@next/swc-linux-arm64-musl/13.0.3-canary.4:
resolution: {integrity: sha512-Ll2nV3pbCi3qL9o+6zxEuQAqqk8yPLk1TJ7+G8fTmm1vpjMjdV8eBiXiZVGyweRBhurhHmeSdh9JtpUFuPvDRA==} resolution: {integrity: sha512-iZTyAMbQiI0kng46mVp9XKscv59STqLbIVs6pSD3pnrBqKUh4SECQ6Z2r6Y4/H65ig64x6hvdk3KbG71UU+Kaw==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [linux] os: [linux]
@ -302,8 +318,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-gnu/13.0.3-canary.2: /@next/swc-linux-x64-gnu/13.0.3-canary.4:
resolution: {integrity: sha512-TARNMLz9+Ab2rEiuk/ulYULLDWw6zMc4yH2vFXdwckod9tWUyxptAMUz2umtKwyf6lmYUv4+IfZPJgUs0lr5Bw==} resolution: {integrity: sha512-2yYi/bjxf5jHJPTvnC6WbomgETkLWaNY+CEC2Ci1HV3xNVm1/4LiKB0KoDZGUWMBDjAQHO9LmTZS8+P4Q/wubA==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -311,8 +327,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-linux-x64-musl/13.0.3-canary.2: /@next/swc-linux-x64-musl/13.0.3-canary.4:
resolution: {integrity: sha512-Wbd1Ufm9NRSf+xl9kOfe5St06xHN1DHT0KrQc+cT2QKn9ZavASM/Vu2PM3gt4T/2Gqdv663WdbpEuX97wn3abQ==} resolution: {integrity: sha512-DuT/jlTSyZDMPWDWpVqxkLJqGytXYnbIbZ8T+XRbOihDy8p4HwaKZW9ZcHM04lSnOmxwXFHRR5Exx4y5cQOH+A==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [linux] os: [linux]
@ -320,8 +336,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-arm64-msvc/13.0.3-canary.2: /@next/swc-win32-arm64-msvc/13.0.3-canary.4:
resolution: {integrity: sha512-d/SiJzQvm+ggFhCBly4VuOUio0OXx5NKLabSw9AcxEK11/V6YGEFNVdPw1q059/eBi3S0mlRBBnowKuJiWGbtg==} resolution: {integrity: sha512-TW1wLzOorp0IhBf2u1XiJ+8OmGWSUID8zWISwyW74oWuNIhpvzbgmCbjFqlfX9xUxAY6tVcx2TfOc5lmsIoaEg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [arm64] cpu: [arm64]
os: [win32] os: [win32]
@ -329,8 +345,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-ia32-msvc/13.0.3-canary.2: /@next/swc-win32-ia32-msvc/13.0.3-canary.4:
resolution: {integrity: sha512-HytAShDnSnY1FkCpsy+t2V09H1Z9ydeZeg8QrLwub26bPWAcDZe77ECVR4rdIHqP4KHBwtAOM8UIZWrexlLggw==} resolution: {integrity: sha512-cCbuBq8ua9u/bpJ0TvyTrEZXNhrzR0R0z/h3gitw+8VUQG4xREwfn3od0J9XjeL0RQ4QbtgorVE2yw9JZ5pOdg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [ia32] cpu: [ia32]
os: [win32] os: [win32]
@ -338,8 +354,8 @@ packages:
dev: false dev: false
optional: true optional: true
/@next/swc-win32-x64-msvc/13.0.3-canary.2: /@next/swc-win32-x64-msvc/13.0.3-canary.4:
resolution: {integrity: sha512-TPH7wQSLXbeWuwkGFASMkCmE2Q7Tt/S8gTOgC0Y4rJf1yw5K+YtubTZKmmEZ13Aq+fQtqg3NkPO9Rrq4OZpuGw==} resolution: {integrity: sha512-kWfN2WhqxwkaySEddUjm2xJKKdeaIE1/UZXFNCaU5aSZFTn1I4yVjjL40tMCfcppqYbY58X6c5UocOviLcKbrg==}
engines: {node: '>= 10'} engines: {node: '>= 10'}
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
@ -368,6 +384,10 @@ packages:
fastq: 1.13.0 fastq: 1.13.0
dev: true dev: true
/@panva/hkdf/1.0.2:
resolution: {integrity: sha512-MSAs9t3Go7GUkMhpKC44T58DJ5KGk2vBo+h1cqQeqlMfdGkxaVB78ZWpv9gYi/g2fa4sopag9gJsNvS8XGgWJA==}
dev: false
/@polka/url/1.0.0-next.21: /@polka/url/1.0.0-next.21:
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
dev: true dev: true
@ -375,8 +395,8 @@ packages:
/@popperjs/core/2.11.6: /@popperjs/core/2.11.6:
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==} resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
/@prisma/client/4.6.0_prisma@4.6.0: /@prisma/client/4.6.1_prisma@4.6.1:
resolution: {integrity: sha512-D9LaQinDxOHinRpcJTw2tjMtjhc9HTP+aF1IRd2oLldp/8TiwIfxK8x17OhBBiX4y1PzbJXXET7kS+5wB3es/w==} resolution: {integrity: sha512-M1+NNrMzqaOIxT7PBGcTs3IZo7d1EW/+gVQd4C4gUgWBDGgD9AcIeZnUSidgWClmpMSgVUdnVORjsWWGUameYA==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
requiresBuild: true requiresBuild: true
peerDependencies: peerDependencies:
@ -385,16 +405,16 @@ packages:
prisma: prisma:
optional: true optional: true
dependencies: dependencies:
'@prisma/engines-version': 4.6.0-53.2e719efb80b56a3f32d18a62489de95bb9c130e3 '@prisma/engines-version': 4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32
prisma: 4.6.0 prisma: 4.6.1
dev: false dev: false
/@prisma/engines-version/4.6.0-53.2e719efb80b56a3f32d18a62489de95bb9c130e3: /@prisma/engines-version/4.6.1-3.694eea289a8462c80264df36757e4fdc129b1b32:
resolution: {integrity: sha512-0CTnfEuUbLlO6n1fM89ERDbSwI4LoyZn+1OKVSwG+aVqohj34+mXRfwOWIM0ONtYtLGGBpddvQAnAZkg+cgS6g==} resolution: {integrity: sha512-HUCmkXAU2jqp2O1RvNtbE+seLGLyJGEABZS/R38rZjSAafAy0WzBuHq+tbZMnD+b5OSCsTVtIPVcuvx1ySxcWQ==}
dev: false dev: false
/@prisma/engines/4.6.0: /@prisma/engines/4.6.1:
resolution: {integrity: sha512-S+72PAl0zTCbIGou1uXD/McvzdtP+bjOs0LRmGZfcOQcVqR9x/0f6Z+dqpUU0zIcqHEl+0DOB8UXaTwRvssFsQ==} resolution: {integrity: sha512-3u2/XxvxB+Q7cMXHnKU0CpBiUK1QWqpgiBv28YDo1zOIJE3FCF8DI2vrp6vuwjGt5h0JGXDSvmSf4D4maVjJdw==}
requiresBuild: true requiresBuild: true
/@rushstack/eslint-patch/1.2.0: /@rushstack/eslint-patch/1.2.0:
@ -573,14 +593,14 @@ packages:
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
dev: true dev: true
/@wits/next-themes/0.2.7_qjr36eup74ongf7bl2iopfchwe: /@wits/next-themes/0.2.7_hsmqkug4agizydugca45idewda:
resolution: {integrity: sha512-CpmNH3RRqf2w0i1Xbrz5GKNE/d5gMq1oBlGpofY9LWcjH225nUgrxP15wKRITRAbn68ERDbsBGEBiaRECTmQag==} resolution: {integrity: sha512-CpmNH3RRqf2w0i1Xbrz5GKNE/d5gMq1oBlGpofY9LWcjH225nUgrxP15wKRITRAbn68ERDbsBGEBiaRECTmQag==}
peerDependencies: peerDependencies:
next: '*' next: '*'
react: '*' react: '*'
react-dom: '*' react-dom: '*'
dependencies: dependencies:
next: 13.0.3-canary.2_biqbaboplfbrettd7655fr4n2y next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0_react@18.2.0 react-dom: 18.2.0_react@18.2.0
dev: false dev: false
@ -973,6 +993,11 @@ packages:
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
dev: false dev: false
/cookie/0.5.0:
resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==}
engines: {node: '>= 0.6'}
dev: false
/cookies-next/2.1.1: /cookies-next/2.1.1:
resolution: {integrity: sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==} resolution: {integrity: sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==}
dependencies: dependencies:
@ -2271,6 +2296,10 @@ packages:
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
dev: true dev: true
/jose/4.11.0:
resolution: {integrity: sha512-wLe+lJHeG8Xt6uEubS4x0LVjS/3kXXu9dGoj9BNnlhYq7Kts0Pbb2pvv5KiI0yaKH/eaiR0LUOBhOVo9ktd05A==}
dev: false
/js-sdsl/4.1.5: /js-sdsl/4.1.5:
resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==}
dev: true dev: true
@ -2677,6 +2706,32 @@ packages:
dev: true dev: true
optional: true optional: true
/next-auth/4.16.4_hsmqkug4agizydugca45idewda:
resolution: {integrity: sha512-KXW578+ER1u5RcWLwCHMdb/RIBIO6JM8r6xlf9RIPSKzkvDcX9FHiZfJS2vOq/SurHXPJZc4J3OS4IDJpF74Dw==}
engines: {node: ^12.19.0 || ^14.15.0 || ^16.13.0 || ^18.12.0}
peerDependencies:
next: ^12.2.5 || ^13
nodemailer: ^6.6.5
react: ^17.0.2 || ^18
react-dom: ^17.0.2 || ^18
peerDependenciesMeta:
nodemailer:
optional: true
dependencies:
'@babel/runtime': 7.20.1
'@panva/hkdf': 1.0.2
cookie: 0.5.0
jose: 4.11.0
next: 13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y
oauth: 0.9.15
openid-client: 5.3.0
preact: 10.11.2
preact-render-to-string: 5.2.6_preact@10.11.2
react: 18.2.0
react-dom: 18.2.0_react@18.2.0
uuid: 8.3.2
dev: false
/next-unused/0.0.6: /next-unused/0.0.6:
resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==} resolution: {integrity: sha512-dHFNNBanFq4wvYrULtsjfWyZ6BzOnr5VYI9EYMGAZYF2vkAhFpj2JOuT5Wu2o3LbFSG92PmAZnSUF/LstF82pA==}
hasBin: true hasBin: true
@ -2688,8 +2743,8 @@ packages:
- supports-color - supports-color
dev: true dev: true
/next/13.0.3-canary.2_biqbaboplfbrettd7655fr4n2y: /next/13.0.3-canary.4_biqbaboplfbrettd7655fr4n2y:
resolution: {integrity: sha512-Qr19ElEa+ljqu56t4AoiZ6uld7jvMa9KbDFhXBcKQQ4/DaRGvLsoWDw9l3QADBhsFSegAon0NE7eI1IAP+M1pQ==} resolution: {integrity: sha512-GCf0loggwGvPXeDfYMtg36HByukmALnldQZMIdQnGcJtFHRQsWrprvrTEfqTENU5UOZSYbTdJRdL1Y8QOyymWw==}
engines: {node: '>=14.6.0'} engines: {node: '>=14.6.0'}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@ -2706,7 +2761,7 @@ packages:
sass: sass:
optional: true optional: true
dependencies: dependencies:
'@next/env': 13.0.3-canary.2 '@next/env': 13.0.3-canary.4
'@swc/helpers': 0.4.11 '@swc/helpers': 0.4.11
caniuse-lite: 1.0.30001431 caniuse-lite: 1.0.30001431
postcss: 8.4.14 postcss: 8.4.14
@ -2715,19 +2770,19 @@ packages:
styled-jsx: 5.1.0_react@18.2.0 styled-jsx: 5.1.0_react@18.2.0
use-sync-external-store: 1.2.0_react@18.2.0 use-sync-external-store: 1.2.0_react@18.2.0
optionalDependencies: optionalDependencies:
'@next/swc-android-arm-eabi': 13.0.3-canary.2 '@next/swc-android-arm-eabi': 13.0.3-canary.4
'@next/swc-android-arm64': 13.0.3-canary.2 '@next/swc-android-arm64': 13.0.3-canary.4
'@next/swc-darwin-arm64': 13.0.3-canary.2 '@next/swc-darwin-arm64': 13.0.3-canary.4
'@next/swc-darwin-x64': 13.0.3-canary.2 '@next/swc-darwin-x64': 13.0.3-canary.4
'@next/swc-freebsd-x64': 13.0.3-canary.2 '@next/swc-freebsd-x64': 13.0.3-canary.4
'@next/swc-linux-arm-gnueabihf': 13.0.3-canary.2 '@next/swc-linux-arm-gnueabihf': 13.0.3-canary.4
'@next/swc-linux-arm64-gnu': 13.0.3-canary.2 '@next/swc-linux-arm64-gnu': 13.0.3-canary.4
'@next/swc-linux-arm64-musl': 13.0.3-canary.2 '@next/swc-linux-arm64-musl': 13.0.3-canary.4
'@next/swc-linux-x64-gnu': 13.0.3-canary.2 '@next/swc-linux-x64-gnu': 13.0.3-canary.4
'@next/swc-linux-x64-musl': 13.0.3-canary.2 '@next/swc-linux-x64-musl': 13.0.3-canary.4
'@next/swc-win32-arm64-msvc': 13.0.3-canary.2 '@next/swc-win32-arm64-msvc': 13.0.3-canary.4
'@next/swc-win32-ia32-msvc': 13.0.3-canary.2 '@next/swc-win32-ia32-msvc': 13.0.3-canary.4
'@next/swc-win32-x64-msvc': 13.0.3-canary.2 '@next/swc-win32-x64-msvc': 13.0.3-canary.4
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
- babel-plugin-macros - babel-plugin-macros
@ -2786,10 +2841,19 @@ packages:
set-blocking: 2.0.0 set-blocking: 2.0.0
dev: false dev: false
/oauth/0.9.15:
resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==}
dev: false
/object-assign/4.1.1: /object-assign/4.1.1:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
/object-hash/2.2.0:
resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==}
engines: {node: '>= 6'}
dev: false
/object-inspect/1.12.2: /object-inspect/1.12.2:
resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==} resolution: {integrity: sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==}
dev: true dev: true
@ -2843,6 +2907,11 @@ packages:
es-abstract: 1.20.4 es-abstract: 1.20.4
dev: true dev: true
/oidc-token-hash/5.0.1:
resolution: {integrity: sha512-EvoOtz6FIEBzE+9q253HsLCVRiK/0doEJ2HCvvqMQb3dHZrP3WlJKYtJ55CRTw4jmYomzH4wkPuCj/I3ZvpKxQ==}
engines: {node: ^10.13.0 || >=12.0.0}
dev: false
/once/1.4.0: /once/1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies: dependencies:
@ -2860,6 +2929,15 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/openid-client/5.3.0:
resolution: {integrity: sha512-SykPCeZBZ/SxiBH5AWynvFUIDX3//2pgwc/3265alUmGHeCN03+X8uP+pHOVnCXCKfX/XOhO90qttAQ76XcGxA==}
dependencies:
jose: 4.11.0
lru-cache: 6.0.0
object-hash: 2.2.0
oidc-token-hash: 5.0.1
dev: false
/optionator/0.8.3: /optionator/0.8.3:
resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==} resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
engines: {node: '>= 0.8.0'} engines: {node: '>= 0.8.0'}
@ -3053,6 +3131,19 @@ packages:
source-map-js: 1.0.2 source-map-js: 1.0.2
dev: true dev: true
/preact-render-to-string/5.2.6_preact@10.11.2:
resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==}
peerDependencies:
preact: '>=10'
dependencies:
preact: 10.11.2
pretty-format: 3.8.0
dev: false
/preact/10.11.2:
resolution: {integrity: sha512-skAwGDFmgxhq1DCBHke/9e12ewkhc7WYwjuhHB8HHS8zkdtITXLRmUMTeol2ldxvLwYtwbFeifZ9uDDWuyL4Iw==}
dev: false
/prebuild-install/7.1.1: /prebuild-install/7.1.1:
resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==} resolution: {integrity: sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3133,6 +3224,10 @@ packages:
hasBin: true hasBin: true
dev: true dev: true
/pretty-format/3.8.0:
resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==}
dev: false
/pretty-ms/7.0.1: /pretty-ms/7.0.1:
resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==} resolution: {integrity: sha512-973driJZvxiGOQ5ONsFhOF/DtzPMOMtgC11kCpUrPGMTgqp2q/1gwzCquocrN33is0VZ5GFHXZYMM9l6h67v2Q==}
engines: {node: '>=10'} engines: {node: '>=10'}
@ -3148,13 +3243,13 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/prisma/4.6.0: /prisma/4.6.1:
resolution: {integrity: sha512-TAnObUMGCM9NLt9nsRs1WWYQGPKsJOK8bN/7gSAnBcYIxMCFFDe+XtFYJbyTzsJZ/i+0rH4zg8au3m7HX354LA==} resolution: {integrity: sha512-BR4itMCuzrDV4tn3e2TF+nh1zIX/RVU0isKtKoN28ADeoJ9nYaMhiuRRkFd2TZN8+l/XfYzoRKyHzUFXLQhmBQ==}
engines: {node: '>=14.17'} engines: {node: '>=14.17'}
hasBin: true hasBin: true
requiresBuild: true requiresBuild: true
dependencies: dependencies:
'@prisma/engines': 4.6.0 '@prisma/engines': 4.6.1
/process-nextick-args/2.0.1: /process-nextick-args/2.0.1:
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
@ -3523,6 +3618,10 @@ packages:
dependencies: dependencies:
lru-cache: 6.0.0 lru-cache: 6.0.0
/server-only/0.0.1:
resolution: {integrity: sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==}
dev: false
/set-blocking/2.0.0: /set-blocking/2.0.0:
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
dev: false dev: false
@ -4024,6 +4123,11 @@ packages:
/util-deprecate/1.0.2: /util-deprecate/1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/uuid/8.3.2:
resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==}
hasBin: true
dev: false
/walkdir/0.4.1: /walkdir/0.4.1:
resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==} resolution: {integrity: sha512-3eBwRyEln6E1MSzcxcVpQIhRG8Q1jLvEqRmCZqS3dsfXEDR/AhOF4d+jHg1qvDCpYaVRZjENPQyrVxAkQqxPgQ==}
engines: {node: '>=6.0.0'} engines: {node: '>=6.0.0'}

View file

@ -1,73 +0,0 @@
-- 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");

View file

@ -1,19 +0,0 @@
-- 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;

View file

@ -1,28 +0,0 @@
/*
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;

View file

@ -1,32 +0,0 @@
/*
Warnings:
- You are about to drop the `PostAuthors` table. If the table is not empty, all the data it contains will be lost.
- Added the required column `authorId` to the `Posts` table without a default value. This is not possible if the table is not empty.
*/
-- DropTable
PRAGMA foreign_keys=off;
DROP TABLE "PostAuthors";
PRAGMA foreign_keys=on;
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_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,
"authorId" TEXT NOT NULL
);
INSERT INTO "new_Posts" ("createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility") SELECT "createdAt", "deletedAt", "description", "expiresAt", "id", "parentId", "password", "title", "updatedAt", "visibility" FROM "Posts";
DROP TABLE "Posts";
ALTER TABLE "new_Posts" RENAME TO "Posts";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View file

@ -1,3 +0,0 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View file

@ -1,10 +1,12 @@
generator client { generator client {
provider = "prisma-client-js" provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
} }
datasource db { datasource db {
provider = "sqlite" provider = "postgresql"
url = env("DATABASE_URL") url = env("DATABASE_URL")
referentialIntegrity = "prisma"
} }
model AuthTokens { model AuthTokens {
@ -16,7 +18,7 @@ model AuthTokens {
deletedAt DateTime? deletedAt DateTime?
userId String userId String
// TODO: verify this isn't necessary / is replaced by an implicit m-n relation // TODO: verify this isn't necessary / is replaced by an implicit m-n relation
// users User[] @relation(fields: [userId], references: [id], onDelete: Cascade) // users DriftUser[] @relation(fields: [userId], references: [id], onDelete: Cascade)
@@id([id, token]) @@id([id, token])
// make id and token keys // make id and token keys
@ -31,30 +33,16 @@ model File {
id String @id @default(cuid()) id String @id @default(cuid())
title String title String
content String content String
sha String sha String @unique
html String html String
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
deletedAt DateTime? deletedAt DateTime?
userId String userId String
postId String postId String
// posts Post[] @relation(fields: [postId], references: [id], onDelete: Cascade) post Post @relation(fields: [postId], references: [id], onDelete: Cascade)
// users User[] @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("Files") @@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 { model Post {
@ -68,25 +56,72 @@ model Post {
expiresAt DateTime? expiresAt DateTime?
parentId String? parentId String?
description String? description String?
authorId String author User? @relation(fields: [authorId], references: [id])
authorId String?
files File[]
@@map("Posts") @@map("posts")
}
// Next auth stuff, from https://next-auth.js.org/adapters/prisma
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
createdAt DateTime @default(now()) @map(name: "created_at")
updatedAt DateTime @default(now()) @map(name: "updated_at")
// https://next-auth.js.org/providers/github
refresh_token_expires_in Int?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map(name: "accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
} }
model User { model User {
id String @id @default(cuid()) id String @id @default(cuid())
username String name String?
password String email String? @unique
createdAt DateTime @default(now()) emailVerified DateTime?
updatedAt DateTime @updatedAt image String?
deletedAt DateTime?
role String? @default("user")
email String?
displayName String?
bio String?
// AuthTokens AuthTokens[]
// files File[]
// post_authors PostToAuthors[]
@@map("Users") accounts Account[]
sessions Session[]
// custom fields
posts Post[]
username String? @unique
role String? @default("user")
password String? @db.Text
@@map("users")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
@@map("verification_tokens")
} }

20
client/types/next-auth.d.ts vendored Normal file
View file

@ -0,0 +1,20 @@
import { User } from "next-auth"
import { JWT } from "next-auth/jwt"
type UserId = string
declare module "next-auth/jwt" {
interface JWT {
id: UserId
role: string
}
}
declare module "next-auth" {
interface Session {
user: User & {
id: UserId
role: string
}
}
}