From 95d1ef31ef88bf3bd174fe3eda3f5cee834b72bf Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Wed, 9 Nov 2022 19:02:06 -0800 Subject: [PATCH] rework cookies --- client/components/auth/index.tsx | 3 +- client/components/new-post/index.tsx | 3 +- client/components/post-page/index.tsx | 3 +- client/lib/api/generate-access-token.ts | 8 +-- client/lib/api/signin.ts | 25 ++++++++ client/lib/constants.ts | 3 + client/lib/hooks/use-signed-in.ts | 9 +-- .../auth/AuthClientContextProvider.tsx | 60 +++++++++++++++++++ client/lib/providers/auth/AuthProvider.tsx | 27 +++++++++ .../auth/AuthServerContextProvider.tsx | 25 ++++++++ client/middleware.ts | 7 ++- client/package.json | 5 +- client/pages/api/auth/signin.ts | 9 +-- client/pages/api/auth/signup.ts | 4 +- client/pages/api/user/[slug].ts | 14 ++--- client/pages/post/[id].tsx | 3 +- client/pnpm-lock.yaml | 39 ++++++------ 17 files changed, 189 insertions(+), 58 deletions(-) create mode 100644 client/lib/api/signin.ts create mode 100644 client/lib/providers/auth/AuthClientContextProvider.tsx create mode 100644 client/lib/providers/auth/AuthProvider.tsx create mode 100644 client/lib/providers/auth/AuthServerContextProvider.tsx diff --git a/client/components/auth/index.tsx b/client/components/auth/index.tsx index f025c9ef..822456a8 100644 --- a/client/components/auth/index.tsx +++ b/client/components/auth/index.tsx @@ -9,6 +9,7 @@ import useSignedIn from "@lib/hooks/use-signed-in" import Input from "@components/input" import Button from "@components/button" import Note from "@components/note" +import { USER_COOKIE_NAME } from "@lib/constants" const NO_EMPTY_SPACE_REGEX = /^\S*$/ const ERROR_MESSAGE = @@ -32,7 +33,7 @@ const Auth = ({ const handleJson = (json: any) => { signin(json.token) - Cookies.set("drift-userid", json.userId) + Cookies.set(USER_COOKIE_NAME, json.userId) router.push("/new") } diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx index 45575f74..12e68a96 100644 --- a/client/components/new-post/index.tsx +++ b/client/components/new-post/index.tsx @@ -16,6 +16,7 @@ import DatePicker from "react-datepicker" import getTitleForPostCopy from "@lib/get-title-for-post-copy" import Description from "./description" import { PostWithFiles } from "app/prisma" +import { USER_COOKIE_NAME } from "@lib/constants" const emptyDoc = { title: "", @@ -144,7 +145,7 @@ const Post = ({ files: docs, visibility, password, - userId: Cookies.get("drift-userid") || "", + userId: Cookies.get(USER_COOKIE_NAME) || "", expiresAt: expiresAt || null, parentId: newPostParent }) diff --git a/client/components/post-page/index.tsx b/client/components/post-page/index.tsx index 49415040..e8e9d294 100644 --- a/client/components/post-page/index.tsx +++ b/client/components/post-page/index.tsx @@ -18,6 +18,7 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge" import Cookies from "js-cookie" import PasswordModalPage from "./password-modal-wrapper" import VisibilityControl from "@components/badges/visibility-control" +import { USER_COOKIE_NAME } from "@lib/constants" type Props = { post: Post @@ -32,7 +33,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => { ) const [isLoading, setIsLoading] = useState(true) const [isOwner] = useState( - post.users ? post.users[0].id === Cookies.get("drift-userid") : false + post.users ? post.users[0].id === Cookies.get(USER_COOKIE_NAME) : false ) const router = useRouter() const isMobile = useMediaQuery("mobile") diff --git a/client/lib/api/generate-access-token.ts b/client/lib/api/generate-access-token.ts index 1d5aa1d9..c52d43cc 100644 --- a/client/lib/api/generate-access-token.ts +++ b/client/lib/api/generate-access-token.ts @@ -3,12 +3,12 @@ import { User } from "@prisma/client" import prisma from "app/prisma" import { sign } from "jsonwebtoken" -export async function generateAccessToken(user: User) { - const token = sign({ id: user.id }, config.jwt_secret, { expiresIn: "2d" }) +export async function generateAndExpireAccessToken(userId: User["id"]) { + const token = sign({ id: userId }, config.jwt_secret, { expiresIn: "2d" }) await prisma.authTokens.create({ data: { - userId: user.id, + userId: userId, token: token } }) @@ -16,7 +16,7 @@ export async function generateAccessToken(user: User) { // TODO: set expiredReason? prisma.authTokens.deleteMany({ where: { - userId: user.id, + userId: userId, token: { not: token } diff --git a/client/lib/api/signin.ts b/client/lib/api/signin.ts new file mode 100644 index 00000000..fa524440 --- /dev/null +++ b/client/lib/api/signin.ts @@ -0,0 +1,25 @@ +import { USER_COOKIE_NAME, TOKEN_COOKIE_NAME } from "@lib/constants" +import { User } from "app/prisma" +import { setCookie } from "cookies-next" +import { NextApiRequest, NextApiResponse } from "next" +import { generateAndExpireAccessToken } from "./generate-access-token" + +export const signin = async ( + userId: User["id"], + req: NextApiRequest, + res: NextApiResponse +) => { + const token = await generateAndExpireAccessToken(userId) + setCookie(USER_COOKIE_NAME, userId, { + maxAge: 30 * 24 * 60 * 60, // 30 days, + req, + res + }) + setCookie(TOKEN_COOKIE_NAME, token, { + maxAge: 30 * 24 * 60 * 60, // 30 days + req, + res + }) + + return token +} diff --git a/client/lib/constants.ts b/client/lib/constants.ts index 5723ba35..5c6fb8cc 100644 --- a/client/lib/constants.ts +++ b/client/lib/constants.ts @@ -121,3 +121,6 @@ export const allowedFileExtensions = [ "log", ...codeFileExtensions ] + +export const TOKEN_COOKIE_NAME = "drift-token" +export const USER_COOKIE_NAME = "drift-userid" diff --git a/client/lib/hooks/use-signed-in.ts b/client/lib/hooks/use-signed-in.ts index 60aacef0..49b6314c 100644 --- a/client/lib/hooks/use-signed-in.ts +++ b/client/lib/hooks/use-signed-in.ts @@ -1,17 +1,18 @@ -import Cookies from "js-cookie" +import { getCookie, setCookie } from "cookies-next" import { useEffect } from "react" import useSharedState from "./use-shared-state" const useSignedIn = () => { + const token = getCookie("drift-token") + const [signedIn, setSignedIn] = useSharedState( "signedIn", - typeof window === "undefined" ? false : !!Cookies.get("drift-token") + typeof window === "undefined" ? false : !!token ) - const token = Cookies.get("drift-token") const signin = (token: string) => { setSignedIn(true) // TODO: investigate SameSite / CORS cookie security - Cookies.set("drift-token", token) + setCookie("drift-token", token) } useEffect(() => { diff --git a/client/lib/providers/auth/AuthClientContextProvider.tsx b/client/lib/providers/auth/AuthClientContextProvider.tsx new file mode 100644 index 00000000..283d653a --- /dev/null +++ b/client/lib/providers/auth/AuthClientContextProvider.tsx @@ -0,0 +1,60 @@ +"use client"; + +import clsx from "clsx"; +import type { + ChangeEventHandler, + FunctionComponent, + PropsWithChildren, +} from "react"; +import Cookies from "js-cookie"; +import React, { useContext, useState, createContext } from "react"; +import { DEFAULT_THEME, Theme, THEME_COOKIE_NAME } from "./theme"; + +const ThemeContext = createContext(null); + +export function useTheme(): Theme { + return useContext(ThemeContext); +} + +interface Props extends PropsWithChildren { + defaultTheme: Theme; +} + +const ThemeClientContextProvider: FunctionComponent = ({ + defaultTheme, + children, +}) => { + const [theme, setTheme] = useState(defaultTheme); + const onChange: ChangeEventHandler = (e) => { + const value = e.target.value as Theme; + setTheme(value); + + if (value === DEFAULT_THEME) { + Cookies.remove(THEME_COOKIE_NAME); + } else { + Cookies.set(THEME_COOKIE_NAME, value); + } + }; + const onReset = () => { + setTheme(DEFAULT_THEME); + Cookies.remove(THEME_COOKIE_NAME); + }; + + return ( +
+
+

Theme Switcher

+ + +
+ {children} +
+ ); +}; + +export default ThemeClientContextProvider; diff --git a/client/lib/providers/auth/AuthProvider.tsx b/client/lib/providers/auth/AuthProvider.tsx new file mode 100644 index 00000000..3a58f29d --- /dev/null +++ b/client/lib/providers/auth/AuthProvider.tsx @@ -0,0 +1,27 @@ +import { FunctionComponent, PropsWithChildren } from "react"; +import ThemeClientContextProvider from "./ThemeClientContextProvider"; +import ThemeServerContextProvider, { + useServerTheme, +} from "./ThemeServerContextProvider"; + +const ThemeProviderWrapper: FunctionComponent = ({ + children, +}) => { + const theme = useServerTheme(); + + return ( + + {children} + + ); +}; + +const ThemeProvider: FunctionComponent = ({ children }) => { + return ( + + {children} + + ); +}; + +export default ThemeProvider; diff --git a/client/lib/providers/auth/AuthServerContextProvider.tsx b/client/lib/providers/auth/AuthServerContextProvider.tsx new file mode 100644 index 00000000..dfdcc0c6 --- /dev/null +++ b/client/lib/providers/auth/AuthServerContextProvider.tsx @@ -0,0 +1,25 @@ +import type { FunctionComponent, PropsWithChildren } from "react"; +// @ts-ignore -- createServerContext is not in @types/react atm +import { useContext, createServerContext } from "react"; +import { cookies } from "next/headers"; +import { Theme, THEME_COOKIE_NAME } from "./theme"; +import { DEFAULT_THEME } from "./theme"; + +const ThemeContext = createServerContext(null); + +export function useServerTheme(): Theme { + return useContext(ThemeContext); +} + +const ThemeServerContextProvider: FunctionComponent = ({ + children, +}) => { + const cookiesList = cookies(); + const theme = cookiesList.get(THEME_COOKIE_NAME) ?? DEFAULT_THEME; + + return ( + {children} + ); +}; + +export default ThemeServerContextProvider; diff --git a/client/middleware.ts b/client/middleware.ts index f938bc1f..d26a7ec6 100644 --- a/client/middleware.ts +++ b/client/middleware.ts @@ -1,11 +1,12 @@ import { NextFetchEvent, NextResponse } from "next/server" import type { NextRequest } from "next/server" +import { TOKEN_COOKIE_NAME, USER_COOKIE_NAME } from "@lib/constants" const PUBLIC_FILE = /\.(.*)$/ export function middleware(req: NextRequest, event: NextFetchEvent) { const pathname = req.nextUrl.pathname - const signedIn = req.cookies.get("drift-token") + const signedIn = req.cookies.get(TOKEN_COOKIE_NAME) const getURL = (pageName: string) => new URL(`/${pageName}`, req.url).href const isPageRequest = !PUBLIC_FILE.test(pathname) && @@ -17,8 +18,8 @@ export function middleware(req: NextRequest, event: NextFetchEvent) { // If you're not signed in we redirect to the home page if (signedIn) { const resp = NextResponse.redirect(getURL("")) - resp.cookies.delete("drift-token") - resp.cookies.delete("drift-userid") + resp.cookies.delete(TOKEN_COOKIE_NAME) + resp.cookies.delete(USER_COOKIE_NAME) const signoutPromise = new Promise((resolve) => { fetch(`${process.env.API_URL}/auth/signout`, { method: "POST", diff --git a/client/package.json b/client/package.json index a419e587..3775430b 100644 --- a/client/package.json +++ b/client/package.json @@ -15,14 +15,11 @@ "@geist-ui/core": "^2.3.8", "@geist-ui/icons": "1.0.2", "@prisma/client": "^4.6.0", - "@types/cookie": "0.5.1", - "@types/js-cookie": "3.0.2", "bcrypt": "^5.1.0", "client-zip": "2.2.1", "clsx": "^1.2.1", - "cookie": "0.5.0", + "cookies-next": "^2.1.1", "dotenv": "16.0.0", - "js-cookie": "3.0.1", "jsonwebtoken": "^8.5.1", "marked": "^4.2.2", "next": "13.0.3-canary.2", diff --git a/client/pages/api/auth/signin.ts b/client/pages/api/auth/signin.ts index f11e07fb..579bd898 100644 --- a/client/pages/api/auth/signin.ts +++ b/client/pages/api/auth/signin.ts @@ -1,9 +1,7 @@ -import config from "@lib/config" import { NextApiRequest, NextApiResponse } from "next" import prisma from "app/prisma" import bcrypt from "bcrypt" -import { generateAccessToken } from "@lib/api/generate-access-token" -import Cookies from "js-cookie" +import { signin } from "@lib/api/signin" export default async function handler( req: NextApiRequest, @@ -29,8 +27,7 @@ export default async function handler( return res.status(401).json({ error: "Unauthorized" }) } - const token = await generateAccessToken(user) - Cookies.set("drift-user", user.id, { path: "/" }) - Cookies.set("drift-token", token, { path: "/" }) + const token = await signin(user.id, req, res); + return res.status(201).json({ token: token, userId: user.id }) } diff --git a/client/pages/api/auth/signup.ts b/client/pages/api/auth/signup.ts index 3f4bf2e5..4f9404f4 100644 --- a/client/pages/api/auth/signup.ts +++ b/client/pages/api/auth/signup.ts @@ -2,7 +2,7 @@ import config from "@lib/config" import { NextApiRequest, NextApiResponse } from "next" import prisma from "app/prisma" import bcrypt, { genSalt } from "bcrypt" -import { generateAccessToken } from "@lib/api/generate-access-token" +import { generateAndExpireAccessToken } from "@lib/api/generate-access-token" export default async function handler( req: NextApiRequest, @@ -35,7 +35,7 @@ export default async function handler( }, }) - const token = await generateAccessToken(user) + const token = await generateAndExpireAccessToken(user) return res.status(201).json({ token: token, userId: user.id }) } diff --git a/client/pages/api/user/[slug].ts b/client/pages/api/user/[slug].ts index e4089d48..ea360225 100644 --- a/client/pages/api/user/[slug].ts +++ b/client/pages/api/user/[slug].ts @@ -1,20 +1,14 @@ -import { withJwt } from "@lib/api/jwt" import config from "@lib/config" import { NextApiRequest, NextApiResponse } from "next" -const handleSelf = async ( - req: NextApiRequest, - res: NextApiResponse -) => { - -} +const handleSelf = async (req: NextApiRequest, res: NextApiResponse) => {} const PATH_TO_HANDLER = { - "self": handleRequiresPasscode + self: handleRequiresPasscode } // eslint-disable-next-line import/no-anonymous-default-export -export default withJwt((req: NextApiRequest, res: NextApiResponse) => { +export default (req: NextApiRequest, res: NextApiResponse) => { const { slug } = req.query if (!slug || Array.isArray(slug)) { @@ -29,4 +23,4 @@ export default withJwt((req: NextApiRequest, res: NextApiResponse) => { default: return res.status(405).json({ error: "Method not allowed" }) } -}) +} diff --git a/client/pages/post/[id].tsx b/client/pages/post/[id].tsx index 24894667..aacdfda7 100644 --- a/client/pages/post/[id].tsx +++ b/client/pages/post/[id].tsx @@ -2,6 +2,7 @@ import type { GetServerSideProps } from "next" import type { Post } from "@lib/types" import PostPage from "@components/post-page" +import { USER_COOKIE_NAME } from "@lib/constants" export type PostProps = { post: Post @@ -47,7 +48,7 @@ export const getServerSideProps: GetServerSideProps = async ({ const json = (await post.json()) as Post const isAuthor = json.users?.find( - (user) => user.id === req.cookies["drift-userid"] + (user) => user.id === req.cookies[USER_COOKIE_NAME] ) if (json.visibility === "public" || json.visibility === "unlisted") { diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index abf56175..d8543710 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -6,8 +6,6 @@ specifiers: '@next/bundle-analyzer': 12.1.6 '@prisma/client': ^4.6.0 '@types/bcrypt': ^5.0.0 - '@types/cookie': 0.5.1 - '@types/js-cookie': 3.0.2 '@types/jsonwebtoken': ^8.5.9 '@types/marked': ^4.0.7 '@types/node': 17.0.23 @@ -17,12 +15,11 @@ specifiers: bcrypt: ^5.1.0 client-zip: 2.2.1 clsx: ^1.2.1 - cookie: 0.5.0 + cookies-next: ^2.1.1 cross-env: 7.0.3 dotenv: 16.0.0 eslint: 8.27.0 eslint-config-next: 13.0.2 - js-cookie: 3.0.1 jsonwebtoken: ^8.5.1 marked: ^4.2.2 next: 13.0.3-canary.2 @@ -49,14 +46,11 @@ dependencies: '@geist-ui/core': 2.3.8_biqbaboplfbrettd7655fr4n2y '@geist-ui/icons': 1.0.2_zhza2kbnl2wkkf7vqdl3ton2f4 '@prisma/client': 4.6.0_prisma@4.6.0 - '@types/cookie': 0.5.1 - '@types/js-cookie': 3.0.2 bcrypt: 5.1.0 client-zip: 2.2.1 clsx: 1.2.1 - cookie: 0.5.0 + cookies-next: 2.1.1 dotenv: 16.0.0 - js-cookie: 3.0.1 jsonwebtoken: 8.5.1 marked: 4.2.2 next: 13.0.3-canary.2_biqbaboplfbrettd7655fr4n2y @@ -415,12 +409,8 @@ packages: '@types/node': 17.0.23 dev: true - /@types/cookie/0.5.1: - resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} - dev: false - - /@types/js-cookie/3.0.2: - resolution: {integrity: sha512-6+0ekgfusHftJNYpihfkMu8BWdeHs9EOJuGcSofErjstGPfPGEu9yTu4t460lTzzAMl2cM5zngQJqPMHbbnvYA==} + /@types/cookie/0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} dev: false /@types/json5/0.0.29: @@ -437,6 +427,10 @@ packages: resolution: {integrity: sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==} dev: true + /@types/node/16.18.3: + resolution: {integrity: sha512-jh6m0QUhIRcZpNv7Z/rpN+ZWXOicUUQbSoWks7Htkbb9IjFQj4kzcX/xFCkjstCj5flMsN8FiSvt+q+Tcs4Llg==} + dev: false + /@types/node/17.0.23: resolution: {integrity: sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==} dev: true @@ -961,11 +955,19 @@ packages: resolution: {integrity: sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==} dev: false - /cookie/0.5.0: - resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + /cookie/0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} engines: {node: '>= 0.6'} dev: false + /cookies-next/2.1.1: + resolution: {integrity: sha512-AZGZPdL1hU3jCjN2UMJTGhLOYzNUN9Gm+v8BdptYIHUdwz397Et1p+sZRfvAl8pKnnmMdX2Pk9xDRKCGBum6GA==} + dependencies: + '@types/cookie': 0.4.1 + '@types/node': 16.18.3 + cookie: 0.4.2 + dev: false + /copy-anything/2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} dependencies: @@ -2256,11 +2258,6 @@ packages: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} dev: true - /js-cookie/3.0.1: - resolution: {integrity: sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==} - engines: {node: '>=12'} - dev: false - /js-sdsl/4.1.5: resolution: {integrity: sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==} dev: true