rework cookies

This commit is contained in:
Max Leiter 2022-11-09 19:02:06 -08:00
parent 9b9c3c1d87
commit 95d1ef31ef
17 changed files with 189 additions and 58 deletions

View file

@ -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")
}

View file

@ -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
})

View file

@ -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")

View file

@ -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
}

25
client/lib/api/signin.ts Normal file
View file

@ -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
}

View file

@ -121,3 +121,6 @@ export const allowedFileExtensions = [
"log",
...codeFileExtensions
]
export const TOKEN_COOKIE_NAME = "drift-token"
export const USER_COOKIE_NAME = "drift-userid"

View file

@ -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(() => {

View file

@ -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<Theme | null>(null);
export function useTheme(): Theme {
return useContext(ThemeContext);
}
interface Props extends PropsWithChildren {
defaultTheme: Theme;
}
const ThemeClientContextProvider: FunctionComponent<Props> = ({
defaultTheme,
children,
}) => {
const [theme, setTheme] = useState<Theme>(defaultTheme);
const onChange: ChangeEventHandler<HTMLSelectElement> = (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 (
<div className={clsx(theme === "dark" && "dark")}>
<div className="mb-2">
<h2 className="mb-2 font-bold text-xl">Theme Switcher</h2>
<select value={theme} onChange={onChange} className="mr-2 inline-block">
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<button className="bg-gray-300 p-2" onClick={onReset}>
Reset
</button>
</div>
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
</div>
);
};
export default ThemeClientContextProvider;

View file

@ -0,0 +1,27 @@
import { FunctionComponent, PropsWithChildren } from "react";
import ThemeClientContextProvider from "./ThemeClientContextProvider";
import ThemeServerContextProvider, {
useServerTheme,
} from "./ThemeServerContextProvider";
const ThemeProviderWrapper: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
const theme = useServerTheme();
return (
<ThemeClientContextProvider defaultTheme={theme}>
{children}
</ThemeClientContextProvider>
);
};
const ThemeProvider: FunctionComponent<PropsWithChildren> = ({ children }) => {
return (
<ThemeServerContextProvider>
<ThemeProviderWrapper>{children}</ThemeProviderWrapper>
</ThemeServerContextProvider>
);
};
export default ThemeProvider;

View file

@ -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<Theme | null>(null);
export function useServerTheme(): Theme {
return useContext(ThemeContext);
}
const ThemeServerContextProvider: FunctionComponent<PropsWithChildren> = ({
children,
}) => {
const cookiesList = cookies();
const theme = cookiesList.get(THEME_COOKIE_NAME) ?? DEFAULT_THEME;
return (
<ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
);
};
export default ThemeServerContextProvider;

View file

@ -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",

View file

@ -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",

View file

@ -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 })
}

View file

@ -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 })
}

View file

@ -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" })
}
})
}

View file

@ -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") {

View file

@ -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