From 60d1b031f5d7e844b22cd20bed66bf513670463b Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Fri, 11 Nov 2022 19:17:44 -0800 Subject: [PATCH] use next-auth, add sign in via github, switch to postgres --- client/app/(auth)/signin/page.tsx | 8 +- client/app/(auth)/signup/page.tsx | 8 +- client/app/(home)/home.tsx | 1 + client/app/(home)/page.tsx | 12 +- client/app/(posts)/new/from/[id]/page.tsx | 20 +- client/app/(posts)/new/layout.tsx | 9 +- client/app/(posts)/new/page.tsx | 6 +- .../post => app/(posts)/post/[id]}/[id].tsx | 0 client/app/(posts)/post/[id]/head.tsx | 24 ++ client/app/(profiles)/mine/page.tsx | 20 +- client/app/(profiles)/settings/page.tsx | 38 ++- client/app/admin/page.tsx | 12 +- client/app/layout.tsx | 7 +- client/app/prisma.ts | 117 --------- client/app/root-layout-wrapper.tsx | 21 +- client/components/auth/auth.module.css | 2 + client/components/auth/index.tsx | 80 +++--- client/components/header/header.module.css | 2 +- client/components/header/index.tsx | 38 ++- client/components/new-post/index.tsx | 2 +- client/components/page-seo/index.tsx | 2 +- client/components/post-list/index.tsx | 2 +- client/components/post-list/list-item.tsx | 2 +- client/components/post-page/index.tsx | 10 +- client/components/settings-group/index.tsx | 1 + client/components/settings/index.tsx | 28 --- .../components/settings/sections/profile.tsx | 9 +- client/lib/config.ts | 6 +- client/lib/hooks/use-signed-in.ts | 14 +- client/lib/hooks/use-user-data.ts | 46 ---- client/lib/server/auth.ts | 61 +++++ client/lib/server/generate-access-token.ts | 2 +- client/lib/server/get-html-from-drift-file.ts | 2 +- .../hooks/use-redirect-if-not-authed.ts | 12 - client/lib/server/is-signed-in.ts | 7 - client/lib/server/jwt.ts | 2 +- client/lib/server/prisma.ts | 173 +++++++++++++ client/lib/server/session.ts | 15 ++ client/lib/server/signin.ts | 2 +- client/middleware.ts | 89 +++---- client/package.json | 9 +- client/pages/api/auth/[...nextauth].ts | 4 + .../api/auth/{signin.ts => signin-backup.ts} | 24 +- client/pages/api/auth/signup-backup.ts | 12 + client/pages/api/auth/signup.ts | 45 ---- client/pages/api/file/get-html.ts | 2 +- client/pages/api/user/posts.ts | 2 +- client/pages/api/user/self.ts | 2 +- client/pnpm-lock.yaml | 232 +++++++++++++----- .../20221110002714_init/migration.sql | 73 ------ .../migration.sql | 19 -- .../migration.sql | 28 --- .../migration.sql | 32 --- client/prisma/migrations/migration_lock.toml | 3 - client/prisma/schema.prisma | 115 ++++++--- client/types/next-auth.d.ts | 20 ++ 56 files changed, 824 insertions(+), 710 deletions(-) rename client/{pages/post => app/(posts)/post/[id]}/[id].tsx (100%) create mode 100644 client/app/(posts)/post/[id]/head.tsx delete mode 100644 client/app/prisma.ts delete mode 100644 client/components/settings/index.tsx delete mode 100644 client/lib/hooks/use-user-data.ts create mode 100644 client/lib/server/auth.ts delete mode 100644 client/lib/server/hooks/use-redirect-if-not-authed.ts delete mode 100644 client/lib/server/is-signed-in.ts create mode 100644 client/lib/server/prisma.ts create mode 100644 client/lib/server/session.ts create mode 100644 client/pages/api/auth/[...nextauth].ts rename client/pages/api/auth/{signin.ts => signin-backup.ts} (55%) create mode 100644 client/pages/api/auth/signup-backup.ts delete mode 100644 client/pages/api/auth/signup.ts delete mode 100644 client/prisma/migrations/20221110002714_init/migration.sql delete mode 100644 client/prisma/migrations/20221110003646_auth_token_type_fixes/migration.sql delete mode 100644 client/prisma/migrations/20221110014822_file_optional_fixes/migration.sql delete mode 100644 client/prisma/migrations/20221110042037_post_author_id/migration.sql delete mode 100644 client/prisma/migrations/migration_lock.toml create mode 100644 client/types/next-auth.d.ts diff --git a/client/app/(auth)/signin/page.tsx b/client/app/(auth)/signin/page.tsx index 1e9469f5..71ff0f86 100644 --- a/client/app/(auth)/signin/page.tsx +++ b/client/app/(auth)/signin/page.tsx @@ -1,5 +1,11 @@ import Auth from "@components/auth" +import Header from "@components/header" export default function SignInPage() { - return + return ( + <> +
+ + + ) } diff --git a/client/app/(auth)/signup/page.tsx b/client/app/(auth)/signup/page.tsx index c1da89f4..2584d0be 100644 --- a/client/app/(auth)/signup/page.tsx +++ b/client/app/(auth)/signup/page.tsx @@ -1,4 +1,5 @@ import Auth from "@components/auth" +import Header from "@components/header" import { getRequiresPasscode } from "pages/api/auth/requires-passcode" const getPasscode = async () => { @@ -7,5 +8,10 @@ const getPasscode = async () => { export default async function SignUpPage() { const requiresPasscode = await getPasscode() - return + return ( + <> +
+ + + ) } diff --git a/client/app/(home)/home.tsx b/client/app/(home)/home.tsx index 42bbbd6e..ede2d77b 100644 --- a/client/app/(home)/home.tsx +++ b/client/app/(home)/home.tsx @@ -24,6 +24,7 @@ const Home = ({ width={48} height={48} alt="" + priority /> diff --git a/client/app/(home)/page.tsx b/client/app/(home)/page.tsx index 7fa37be4..4f6f911f 100644 --- a/client/app/(home)/page.tsx +++ b/client/app/(home)/page.tsx @@ -1,3 +1,5 @@ +import Header from "@components/header" +import { getCurrentUser } from "@lib/server/session" import { getWelcomeContent } from "pages/api/welcome" import Home from "./home" @@ -8,6 +10,12 @@ const getWelcomeData = async () => { export default async function Page() { const { content, rendered, title } = await getWelcomeData() - - return + const authed = await getCurrentUser(); + + return ( + <> +
+ + + ) } diff --git a/client/app/(posts)/new/from/[id]/page.tsx b/client/app/(posts)/new/from/[id]/page.tsx index 6db2aa28..153b47fd 100644 --- a/client/app/(posts)/new/from/[id]/page.tsx +++ b/client/app/(posts)/new/from/[id]/page.tsx @@ -1,9 +1,7 @@ import NewPost from "@components/new-post" import { useRouter } from "next/navigation" -import { cookies } from "next/headers" -import { TOKEN_COOKIE_NAME } from "@lib/constants" -import { getPostWithFiles } from "app/prisma" -import { useRedirectIfNotAuthed } from "@lib/server/hooks/use-redirect-if-not-authed" +import { getPostWithFiles } from "@lib/server/prisma" +import Header from "@components/header" const NewFromExisting = async ({ params @@ -14,13 +12,6 @@ const NewFromExisting = async ({ }) => { const { id } = params const router = useRouter() - const cookieList = cookies() - useRedirectIfNotAuthed() - const driftToken = cookieList.get(TOKEN_COOKIE_NAME) - - if (!driftToken) { - return router.push("/signin") - } if (!id) { return router.push("/new") @@ -28,7 +19,12 @@ const NewFromExisting = async ({ const post = await getPostWithFiles(id) - return + return ( + <> +
+ + + ) } export default NewFromExisting diff --git a/client/app/(posts)/new/layout.tsx b/client/app/(posts)/new/layout.tsx index 35dfebc7..2eac9310 100644 --- a/client/app/(posts)/new/layout.tsx +++ b/client/app/(posts)/new/layout.tsx @@ -1,4 +1,11 @@ +import { getCurrentUser } from "@lib/server/session" +import { redirect } from "next/navigation" + export default function NewLayout({ children }: { children: React.ReactNode }) { - // useRedirectIfNotAuthed() + const user = getCurrentUser() + if (!user) { + return redirect("/new") + } + return <>{children} } diff --git a/client/app/(posts)/new/page.tsx b/client/app/(posts)/new/page.tsx index 4bb5e812..c4570825 100644 --- a/client/app/(posts)/new/page.tsx +++ b/client/app/(posts)/new/page.tsx @@ -1,6 +1,10 @@ +import Header from "@components/header" import NewPost from "@components/new-post" import "@styles/react-datepicker.css" -const New = () => +const New = () => <> +
+ + export default New diff --git a/client/pages/post/[id].tsx b/client/app/(posts)/post/[id]/[id].tsx similarity index 100% rename from client/pages/post/[id].tsx rename to client/app/(posts)/post/[id]/[id].tsx diff --git a/client/app/(posts)/post/[id]/head.tsx b/client/app/(posts)/post/[id]/head.tsx new file mode 100644 index 00000000..6e158d00 --- /dev/null +++ b/client/app/(posts)/post/[id]/head.tsx @@ -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 ( + + ) +} diff --git a/client/app/(profiles)/mine/page.tsx b/client/app/(profiles)/mine/page.tsx index 94c59c9b..74f62452 100644 --- a/client/app/(profiles)/mine/page.tsx +++ b/client/app/(profiles)/mine/page.tsx @@ -1,16 +1,26 @@ 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 { getPostsByUser } from "app/prisma" +import { getPostsByUser } from "@lib/server/prisma" 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() { - const userId = cookies().get(USER_COOKIE_NAME)?.value + const userId = (await getCurrentUser())?.id + if (!userId) { - return notFound() + redirect(authOptions.pages?.signIn || "/new") } const posts = await getPostsByUser(userId, true) + const hasMore = false - return + return ( + <> +
+ + + ) } diff --git a/client/app/(profiles)/settings/page.tsx b/client/app/(profiles)/settings/page.tsx index ce14b34f..4d7fc8fd 100644 --- a/client/app/(profiles)/settings/page.tsx +++ b/client/app/(profiles)/settings/page.tsx @@ -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 = () => +export default async function SettingsPage() { + const user = await getCurrentUser() -export default Settings + if (!user) { + redirect(authOptions.pages?.signIn || "/new") + } + + return ( + <> +
+
+

Settings

+ + + + + + +
+ + ) +} diff --git a/client/app/admin/page.tsx b/client/app/admin/page.tsx index 3f3b36bb..ffba5bc1 100644 --- a/client/app/admin/page.tsx +++ b/client/app/admin/page.tsx @@ -1,18 +1,18 @@ import Admin from "@components/admin" 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 { notFound } from "next/navigation" const AdminPage = async () => { - const driftToken = cookies().get(TOKEN_COOKIE_NAME)?.value - if (!driftToken) { + const user = await getCurrentUser() + + if (!user) { return notFound() } - const isAdmin = await isUserAdmin(driftToken) - - if (!isAdmin) { + if (user.role !== "admin") { return notFound() } diff --git a/client/app/layout.tsx b/client/app/layout.tsx index bb0cecaf..1cd14fd0 100644 --- a/client/app/layout.tsx +++ b/client/app/layout.tsx @@ -1,16 +1,17 @@ import "@styles/globals.css" import { ServerThemeProvider } from "next-themes" import { LayoutWrapper } from "./root-layout-wrapper" +import styles from '@styles/Home.module.css'; interface RootLayoutProps { children: React.ReactNode } -export default function RootLayout({ children }: RootLayoutProps) { +export default async function RootLayout({ children }: RootLayoutProps) { return ( @@ -50,7 +51,7 @@ export default function RootLayout({ children }: RootLayoutProps) { Drift - + {children} diff --git a/client/app/prisma.ts b/client/app/prisma.ts deleted file mode 100644 index 319b82ba..00000000 --- a/client/app/prisma.ts +++ /dev/null @@ -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 => { - 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 -export async function getPostsByUser( - userId: string, - includeFiles: true -): Promise -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" -} diff --git a/client/app/root-layout-wrapper.tsx b/client/app/root-layout-wrapper.tsx index 84e64da5..98ddee7a 100644 --- a/client/app/root-layout-wrapper.tsx +++ b/client/app/root-layout-wrapper.tsx @@ -1,12 +1,14 @@ "use client" -import Header from "@components/header" import { CssBaseline, GeistProvider, Page, Themes } from "@geist-ui/core/dist" import { ThemeProvider } from "next-themes" 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 skeletonHighlightColor = "var(--lighter-gray)" @@ -30,7 +32,7 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { dropdownBoxShadow: "0 0 0 1px var(--lighter-gray)", shadowSmall: "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: { gap: "var(--gap)", @@ -59,12 +61,11 @@ export function LayoutWrapper({ children }: { children: React.ReactNode }) { attribute="data-theme" > - - -
- - - {children} + + {children} diff --git a/client/components/auth/auth.module.css b/client/components/auth/auth.module.css index 0b1da78b..257ea46b 100644 --- a/client/components/auth/auth.module.css +++ b/client/components/auth/auth.module.css @@ -14,6 +14,8 @@ flex-direction: column; place-items: center; gap: 10px; + max-width: 300px; + width: 100%; } .formContentSpace { diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index 4dd97337..0df97392 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -4,11 +4,9 @@ import { FormEvent, useState } from "react" import styles from "./auth.module.css" import { useRouter } from "next/navigation" 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 { signIn } from "next-auth/react" +import { Github as GithubIcon } from "@geist-ui/icons" const NO_EMPTY_SPACE_REGEX = /^\S*$/ const ERROR_MESSAGE = "Provide a non empty username and a password with at least 6 characters" @@ -27,29 +25,27 @@ const Auth = ({ const [serverPassword, setServerPassword] = useState("") const [errorMsg, setErrorMsg] = useState("") const signingIn = page === "signin" - const { signin } = useSignedIn() const handleJson = (json: any) => { - signin(json.token) - setCookie(USER_COOKIE_NAME, json.userId) + // setCookie(USER_COOKIE_NAME, json.userId) router.push("/new") } const handleSubmit = async (e: FormEvent) => { e.preventDefault() - if ( - !signingIn && - (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) - ) - return setErrorMsg(ERROR_MESSAGE) - if ( - !signingIn && - requiresServerPassword && - !NO_EMPTY_SPACE_REGEX.test(serverPassword) - ) - return setErrorMsg(ERROR_MESSAGE) - else setErrorMsg("") + // if ( + // !signingIn && + // (!NO_EMPTY_SPACE_REGEX.test(username) || password.length < 6) + // ) + // return setErrorMsg(ERROR_MESSAGE) + // if ( + // !signingIn && + // requiresServerPassword && + // !NO_EMPTY_SPACE_REGEX.test(serverPassword) + // ) + // return setErrorMsg(ERROR_MESSAGE) + // else setErrorMsg("") const reqOpts = { method: "POST", @@ -60,12 +56,18 @@ const Auth = ({ } try { - 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) + // signIn("credentials", { + // callbackUrl: "/new", + // redirect: false, + // username, + // password, + // 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) { setErrorMsg(err.message ?? "Something went wrong") } @@ -77,9 +79,10 @@ const Auth = ({

{signingIn ? "Sign In" : "Sign Up"}

-
+ {/* */} +
- + width="100%" + /> */} + {/* sign in with github */} {requiresServerPassword && ( )} - - + + {/* */}
{signingIn ? ( @@ -125,7 +141,7 @@ const Auth = ({

) : (

- Already have an account?{" "} + Have an account?{" "} Sign in diff --git a/client/components/header/header.module.css b/client/components/header/header.module.css index b0aee771..636b5c47 100644 --- a/client/components/header/header.module.css +++ b/client/components/header/header.module.css @@ -23,7 +23,7 @@ .mobile { position: absolute; - z-index: 1; + z-index: 1000; } .controls { diff --git a/client/components/header/index.tsx b/client/components/header/index.tsx index a53b954b..b7a2582a 100644 --- a/client/components/header/index.tsx +++ b/client/components/header/index.tsx @@ -11,7 +11,6 @@ import { import { useCallback, useEffect, useMemo, useState } from "react" import styles from "./header.module.css" -import useSignedIn from "../../lib/hooks/use-signed-in" import HomeIcon from "@geist-ui/icons/home" 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 SunIcon from "@geist-ui/icons/sun" 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 { usePathname } from "next/navigation" @@ -37,13 +36,13 @@ type Tab = { href?: string } -const Header = () => { +const Header = ({ signedIn = false }) => { const pathname = usePathname() const [expanded, setExpanded] = useState(false) const [, setBodyHidden] = useBodyScroll(null, { scrollLayer: true }) const isMobile = useMediaQuery("xs", { match: "down" }) - const { signedIn: isSignedIn } = useSignedIn() - const userData = useUserData() + // const { status } = useSession() + // const signedIn = status === "authenticated" const [pages, setPages] = useState([]) const { setTheme, resolvedTheme } = useTheme() @@ -76,7 +75,7 @@ const Header = () => { } ] - if (isSignedIn) + if (signedIn) setPages([ { name: "new", @@ -126,20 +125,20 @@ const Header = () => { }, ...defaultPages ]) - if (userData?.role === "admin") { - setPages((pages) => [ - ...pages, - { - name: "admin", - icon: , - value: "admin", - href: "/admin" - } - ]) - } + // if (userData?.role === "admin") { + // setPages((pages) => [ + // ...pages, + // { + // name: "admin", + // icon: , + // value: "admin", + // href: "/admin" + // } + // ]) + // } // TODO: investigate deps causing infinite loop // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isMobile, isSignedIn, resolvedTheme, userData]) + }, [isMobile, resolvedTheme]) const onTabChange = useCallback( (tab: string) => { @@ -172,7 +171,6 @@ const Header = () => { return (