From 9f9766a1dcd91266c483395df0b188d0984c1f67 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Sep 2025 16:12:47 +0000 Subject: [PATCH 1/6] Refactor: Use react-router middleware for auth checks This commit refactors authentication checks to use react-router's middleware system. This centralizes auth logic and improves code organization. Co-authored-by: me --- app/context.ts | 5 + app/middleware.server.ts | 35 +++++ .../_auth+/auth.$provider.callback.test.ts | 17 +- app/routes/_auth+/login.tsx | 7 +- app/routes/_auth+/onboarding.tsx | 11 +- app/routes/_auth+/onboarding_.$provider.tsx | 10 +- app/routes/_auth+/reset-password.tsx | 10 +- app/routes/_auth+/signup.tsx | 5 +- app/routes/_seo+/sitemap[.]xml.ts | 6 +- app/routes/settings+/profile.tsx | 13 +- app/routes/users+/index.tsx | 11 +- package-lock.json | 148 +++++------------- package.json | 10 +- react-router.config.ts | 1 + server/index.ts | 10 +- 15 files changed, 140 insertions(+), 159 deletions(-) create mode 100644 app/context.ts create mode 100644 app/middleware.server.ts diff --git a/app/context.ts b/app/context.ts new file mode 100644 index 000000000..7b5725d5b --- /dev/null +++ b/app/context.ts @@ -0,0 +1,5 @@ +import { createContext } from 'react-router' + +// Holds the authenticated user's ID for routes protected by middleware +export const userIdContext = createContext(null) + diff --git a/app/middleware.server.ts b/app/middleware.server.ts new file mode 100644 index 000000000..2f3e8c6f7 --- /dev/null +++ b/app/middleware.server.ts @@ -0,0 +1,35 @@ +import { redirect, type MiddlewareFunction } from 'react-router' +import { prisma } from '#app/utils/db.server.ts' +import { authSessionStorage } from '#app/utils/session.server.ts' +import { userIdContext } from '#app/context.ts' + +export const requireUserMiddleware: MiddlewareFunction = async ({ + request, + context, +}) => { + const cookie = request.headers.get('cookie') + const session = await authSessionStorage.getSession(cookie) + const sessionId = session.get('sessionId') as string | undefined + if (!sessionId) throw redirect(`/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`) + + const sessionRecord = await prisma.session.findUnique({ + select: { userId: true, expirationDate: true }, + where: { id: sessionId }, + }) + + if (!sessionRecord || sessionRecord.expirationDate < new Date()) { + throw redirect(`/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`) + } + + context.set(userIdContext, sessionRecord.userId) +} + +export const requireAnonymousMiddleware: MiddlewareFunction = async ({ + request, +}) => { + const cookie = request.headers.get('cookie') + const session = await authSessionStorage.getSession(cookie) + const sessionId = session.get('sessionId') as string | undefined + if (sessionId) throw redirect('/') +} + diff --git a/app/routes/_auth+/auth.$provider.callback.test.ts b/app/routes/_auth+/auth.$provider.callback.test.ts index 643e4534c..7a436fa2c 100644 --- a/app/routes/_auth+/auth.$provider.callback.test.ts +++ b/app/routes/_auth+/auth.$provider.callback.test.ts @@ -3,6 +3,7 @@ import { faker } from '@faker-js/faker' import { SetCookie } from '@mjackson/headers' import { http } from 'msw' import { afterEach, expect, test } from 'vitest' +import { RouterContextProvider } from 'react-router' import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts' import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx' @@ -25,7 +26,7 @@ afterEach(async () => { test('a new user goes to onboarding', async () => { const request = await setupRequest() - const response = await loader({ request, params: PARAMS, context: {} }).catch( + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }).catch( (e) => e, ) expect(response).toHaveRedirect('/onboarding/github') @@ -39,7 +40,7 @@ test('when auth fails, send the user to login with a toast', async () => { }), ) const request = await setupRequest() - const response = await loader({ request, params: PARAMS, context: {} }).catch( + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }).catch( (e) => e, ) invariant(response instanceof Response, 'response should be a Response') @@ -60,7 +61,7 @@ test('when a user is logged in, it creates the connection', async () => { sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -96,7 +97,7 @@ test(`when a user is logged in and has already connected, it doesn't do anything sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -111,7 +112,7 @@ test('when a user exists with the same email, create connection and make session const email = githubUser.primaryEmail.toLowerCase() const { userId } = await setupUser({ ...createUser(), email }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) expect(response).toHaveRedirect('/') @@ -155,7 +156,7 @@ test('gives an error if the account is already connected to another user', async sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -178,7 +179,7 @@ test('if a user is not logged in, but the connection exists, make a session', as }, }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) expect(response).toHaveRedirect('/') await expect(response).toHaveSessionForUser(userId) }) @@ -202,7 +203,7 @@ test('if a user is not logged in, but the connection exists and they have enable }, }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: {} }) + const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) const searchParams = new URLSearchParams({ type: twoFAVerificationType, target: userId, diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index fc12b1cca..0e3ee139c 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -11,7 +11,10 @@ import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { login, requireAnonymous } from '#app/utils/auth.server.ts' +import { login } from '#app/utils/auth.server.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, +] import { ProviderConnectionForm, providerNames, @@ -38,12 +41,10 @@ const AuthenticationOptionsSchema = z.object({ }) satisfies z.ZodType<{ options: PublicKeyCredentialRequestOptionsJSON }> export async function loader({ request }: Route.LoaderArgs) { - await requireAnonymous(request) return {} } export async function action({ request }: Route.ActionArgs) { - await requireAnonymous(request) const formData = await request.formData() await checkHoneypot(formData) const submission = await parseWithZod(formData, { diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index a9d05a8e6..70fb3bd1b 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -7,12 +7,10 @@ import { z } from 'zod' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { - checkIsCommonPassword, - requireAnonymous, - sessionKey, - signup, -} from '#app/utils/auth.server.ts' +import { checkIsCommonPassword, sessionKey, signup } from '#app/utils/auth.server.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, +] import { prisma } from '#app/utils/db.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' import { useIsPending } from '#app/utils/misc.tsx' @@ -42,7 +40,6 @@ const SignupFormSchema = z .and(PasswordAndConfirmPasswordSchema) async function requireOnboardingEmail(request: Request) { - await requireAnonymous(request) const verifySession = await verifySessionStorage.getSession( request.headers.get('cookie'), ) diff --git a/app/routes/_auth+/onboarding_.$provider.tsx b/app/routes/_auth+/onboarding_.$provider.tsx index 65aeafa00..bbe39452e 100644 --- a/app/routes/_auth+/onboarding_.$provider.tsx +++ b/app/routes/_auth+/onboarding_.$provider.tsx @@ -17,11 +17,10 @@ import { z } from 'zod' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { - sessionKey, - signupWithConnection, - requireAnonymous, -} from '#app/utils/auth.server.ts' +import { sessionKey, signupWithConnection } from '#app/utils/auth.server.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, +] import { ProviderNameSchema } from '#app/utils/connections.tsx' import { prisma } from '#app/utils/db.server.ts' import { useIsPending } from '#app/utils/misc.tsx' @@ -53,7 +52,6 @@ async function requireData({ request: Request params: Params }) { - await requireAnonymous(request) const verifySession = await verifySessionStorage.getSession( request.headers.get('cookie'), ) diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index 1280e1b7c..12c3b3a28 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -5,11 +5,10 @@ import { data, redirect, Form } from 'react-router' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { - checkIsCommonPassword, - requireAnonymous, - resetUserPassword, -} from '#app/utils/auth.server.ts' +import { checkIsCommonPassword, resetUserPassword } from '#app/utils/auth.server.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, +] import { useIsPending } from '#app/utils/misc.tsx' import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' @@ -24,7 +23,6 @@ export const resetPasswordUsernameSessionKey = 'resetPasswordUsername' const ResetPasswordSchema = PasswordAndConfirmPasswordSchema async function requireResetPasswordUsername(request: Request) { - await requireAnonymous(request) const verifySession = await verifySessionStorage.getSession( request.headers.get('cookie'), ) diff --git a/app/routes/_auth+/signup.tsx b/app/routes/_auth+/signup.tsx index 095a23380..f0f0164b0 100644 --- a/app/routes/_auth+/signup.tsx +++ b/app/routes/_auth+/signup.tsx @@ -8,7 +8,9 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { requireAnonymous } from '#app/utils/auth.server.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, +] import { ProviderConnectionForm, providerNames, @@ -30,7 +32,6 @@ const SignupSchema = z.object({ }) export async function loader({ request }: Route.LoaderArgs) { - await requireAnonymous(request) return null } diff --git a/app/routes/_seo+/sitemap[.]xml.ts b/app/routes/_seo+/sitemap[.]xml.ts index 04d37c31e..e54f2ffca 100644 --- a/app/routes/_seo+/sitemap[.]xml.ts +++ b/app/routes/_seo+/sitemap[.]xml.ts @@ -1,10 +1,12 @@ import { generateSitemap } from '@nasa-gcn/remix-seo' -import { type ServerBuild } from 'react-router' +import { type ServerBuild, RouterContextProvider, createContext } from 'react-router' import { getDomainUrl } from '#app/utils/misc.tsx' import { type Route } from './+types/sitemap[.]xml.ts' +// recreate context key to match the one set in server getLoadContext +export const serverBuildContext = createContext | null>(null) export async function loader({ request, context }: Route.LoaderArgs) { - const serverBuild = (await context.serverBuild) as { build: ServerBuild } + const serverBuild = (await (context as Readonly).get(serverBuildContext)) as { build: ServerBuild } // TODO: This is typeerror is coming up since of the remix-run/server-runtime package. We might need to remove/update that one. // @ts-expect-error diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index f7ab8fcf0..77afcbf59 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -4,12 +4,16 @@ import { Link, Outlet, useMatches } from 'react-router' import { z } from 'zod' import { Spacer } from '#app/components/spacer.tsx' import { Icon } from '#app/components/ui/icon.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' +import { userIdContext } from '#app/context.ts' import { prisma } from '#app/utils/db.server.ts' import { cn } from '#app/utils/misc.tsx' import { useUser } from '#app/utils/user.ts' import { type Route } from './+types/profile.ts' +export const unstable_middleware = [ + (await import('#app/middleware.server.ts')).requireUserMiddleware, +] + export const BreadcrumbHandle = z.object({ breadcrumb: z.any() }) export type BreadcrumbHandle = z.infer @@ -18,10 +22,11 @@ export const handle: BreadcrumbHandle & SEOHandle = { getSitemapEntries: () => null, } -export async function loader({ request }: Route.LoaderArgs) { - const userId = await requireUserId(request) +export async function loader({ context }: Route.LoaderArgs) { + const userId = context.get(userIdContext) as string | null + invariantResponse(Boolean(userId), 'Unauthorized', { status: 401 }) const user = await prisma.user.findUnique({ - where: { id: userId }, + where: { id: userId as string }, select: { username: true }, }) invariantResponse(user, 'User not found', { status: 404 }) diff --git a/app/routes/users+/index.tsx b/app/routes/users+/index.tsx index d6a5e1221..edc1ac45c 100644 --- a/app/routes/users+/index.tsx +++ b/app/routes/users+/index.tsx @@ -1,4 +1,4 @@ -import { searchUsers } from '@prisma/client/sql' +// using $queryRawUnsafe for LIKE query construction import { Img } from 'openimg/react' import { redirect, Link } from 'react-router' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' @@ -15,7 +15,10 @@ export async function loader({ request }: Route.LoaderArgs) { } const like = `%${searchTerm ?? ''}%` - const users = await prisma.$queryRawTyped(searchUsers(like)) + const users = await prisma.$queryRawUnsafe<{ id: string; username: string; name: string | null; imageObjectKey: string | null }[]>( + `SELECT id, username, name, (SELECT "objectKey" FROM "Image" WHERE "userId" = "User"."id" LIMIT 1) as "imageObjectKey" FROM "User" WHERE username ILIKE $1 OR name ILIKE $1 ORDER BY username ASC`, + like, + ) return { status: 'idle', users } as const } @@ -49,7 +52,7 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) { > {user.nameNo users found

) ) : loaderData.status === 'error' ? ( - + ) : null} diff --git a/package-lock.json b/package-lock.json index 02c417d12..77094c54f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,9 +30,9 @@ "@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-tooltip": "^1.2.4", "@react-email/components": "0.0.38", - "@react-router/express": "^7.8.1", - "@react-router/node": "^7.8.1", - "@react-router/remix-routes-option-adapter": "^7.8.1", + "@react-router/express": "^7.9.0", + "@react-router/node": "^7.9.0", + "@react-router/remix-routes-option-adapter": "^7.9.0", "@remix-run/server-runtime": "^2.16.5", "@sentry/profiling-node": "^9.32.0", "@sentry/react-router": "^9.32.0", @@ -67,7 +67,7 @@ "qrcode": "^1.5.4", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router": "^7.8.1", + "react-router": "^7.9.0", "remix-auth": "^4.2.0", "remix-auth-github": "^3.0.2", "remix-utils": "^8.5.0", @@ -85,7 +85,7 @@ "@epic-web/config": "^1.20.1", "@faker-js/faker": "^9.7.0", "@playwright/test": "^1.52.0", - "@react-router/dev": "^7.8.1", + "@react-router/dev": "^7.9.0", "@sly-cli/sly": "^2.1.1", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -2595,7 +2595,8 @@ "node_modules/@mjackson/node-fetch-server": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.2.0.tgz", - "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==" + "integrity": "sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==", + "license": "MIT" }, "node_modules/@mswjs/interceptors": { "version": "0.39.2", @@ -4706,10 +4707,11 @@ } }, "node_modules/@react-router/dev": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.8.1.tgz", - "integrity": "sha512-ESFe7DbMvCvl7e8N7L9NmI64VJGNCc60/VX1DUZYw/jFfzA5098/6D1aUojcxyVYBbMbVTfw0xmEvD4CsJzy1Q==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@react-router/dev/-/dev-7.9.0.tgz", + "integrity": "sha512-7Jo4PTfcQHNxYEvSU3QSBSG3QwDKgYIw1uD/jz7n3fyL9k6KCB0zgFsMyGDnhrDh9AYL35WC9LF+PcQhGbs//w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/core": "^7.27.7", "@babel/generator": "^7.27.5", @@ -4719,8 +4721,7 @@ "@babel/traverse": "^7.27.7", "@babel/types": "^7.27.7", "@npmcli/package-json": "^4.0.1", - "@react-router/node": "7.8.1", - "@vitejs/plugin-rsc": "0.4.11", + "@react-router/node": "7.9.0", "arg": "^5.0.1", "babel-dead-code-elimination": "^1.0.6", "chokidar": "^4.0.0", @@ -4747,8 +4748,9 @@ "node": ">=20.0.0" }, "peerDependencies": { - "@react-router/serve": "^7.8.1", - "react-router": "^7.8.1", + "@react-router/serve": "^7.9.0", + "@vitejs/plugin-rsc": "*", + "react-router": "^7.9.0", "typescript": "^5.1.0", "vite": "^5.1.0 || ^6.0.0 || ^7.0.0", "wrangler": "^3.28.2 || ^4.0.0" @@ -4757,6 +4759,9 @@ "@react-router/serve": { "optional": true }, + "@vitejs/plugin-rsc": { + "optional": true + }, "typescript": { "optional": true }, @@ -4766,18 +4771,19 @@ } }, "node_modules/@react-router/express": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.8.1.tgz", - "integrity": "sha512-Oq+l1eOex6TE1uAixM177YGF0yhYCqMoqvLQIjAGz4bfpCui6UewSDR6FSBNm+vub2OB06B5ARk6W4BOzf2ZcQ==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@react-router/express/-/express-7.9.0.tgz", + "integrity": "sha512-YftJtPhLazjM2k5h+2Be9U+GZ4MtH7FSF1X2GNEEqoEmiHZDnh2nneXqNkEGkZT9EDWbGLjcRhLcZhA6Q2+dag==", + "license": "MIT", "dependencies": { - "@react-router/node": "7.8.1" + "@react-router/node": "7.9.0" }, "engines": { "node": ">=20.0.0" }, "peerDependencies": { "express": "^4.17.1 || ^5", - "react-router": "7.8.1", + "react-router": "7.9.0", "typescript": "^5.1.0" }, "peerDependenciesMeta": { @@ -4787,9 +4793,10 @@ } }, "node_modules/@react-router/node": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.8.1.tgz", - "integrity": "sha512-NC8eVQir2CRdcokzyyBsfxdq85Yu8B5XynDt581CzjBOreHAFfqIsNjGnqmg+aqBLiknQb2De9fH/TjyeYNeqw==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@react-router/node/-/node-7.9.0.tgz", + "integrity": "sha512-sqi4JAaJN9AO/Aq+wEfu2YLrrN/gKGN9YPm4cL415GtC7ush5V+yrLBISzyaSpz9isRaeNy16mp7MiCrfIWRwg==", + "license": "MIT", "dependencies": { "@mjackson/node-fetch-server": "^0.2.0" }, @@ -4797,7 +4804,7 @@ "node": ">=20.0.0" }, "peerDependencies": { - "react-router": "7.8.1", + "react-router": "7.9.0", "typescript": "^5.1.0" }, "peerDependenciesMeta": { @@ -4807,14 +4814,15 @@ } }, "node_modules/@react-router/remix-routes-option-adapter": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/@react-router/remix-routes-option-adapter/-/remix-routes-option-adapter-7.8.1.tgz", - "integrity": "sha512-++7gmQlGL/7Y0I3YTvYLJTeXEa0kZ0yhecBCTdherFIg63zAzioWAAyiP0iNGEoS78Sz4JxiGjZUgO/eOh98gg==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/@react-router/remix-routes-option-adapter/-/remix-routes-option-adapter-7.9.0.tgz", + "integrity": "sha512-C5FNI25/AiAv7b9cqUB8nnwTVVHwW7VUhZuIB/NQAcmT4AGwxylVHc/+kEPQHbrCImGVIpv8lIL/ENXLvbdSpg==", + "license": "MIT", "engines": { "node": ">=20.0.0" }, "peerDependencies": { - "@react-router/dev": "^7.8.1", + "@react-router/dev": "^7.9.0", "typescript": "^5.1.0" }, "peerDependenciesMeta": { @@ -7445,47 +7453,6 @@ "node": ">=0.10.0" } }, - "node_modules/@vitejs/plugin-rsc": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-rsc/-/plugin-rsc-0.4.11.tgz", - "integrity": "sha512-+4H4wLi+Y9yF58znBfKgGfX8zcqUGt8ngnmNgzrdGdF1SVz7EO0sg7WnhK5fFVHt6fUxsVEjmEabsCWHKPL1Tw==", - "dev": true, - "dependencies": { - "@mjackson/node-fetch-server": "^0.7.0", - "es-module-lexer": "^1.7.0", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17", - "periscopic": "^4.0.2", - "turbo-stream": "^3.1.0", - "vitefu": "^1.1.1" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*", - "vite": "*" - } - }, - "node_modules/@vitejs/plugin-rsc/node_modules/@mjackson/node-fetch-server": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@mjackson/node-fetch-server/-/node-fetch-server-0.7.0.tgz", - "integrity": "sha512-un8diyEBKU3BTVj3GzlTPA1kIjCkGdD+AMYQy31Gf9JCkfoZzwgJ79GUtHrF2BN3XPNMLpubbzPcxys+a3uZEw==", - "dev": true - }, - "node_modules/@vitejs/plugin-rsc/node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" - } - }, - "node_modules/@vitejs/plugin-rsc/node_modules/turbo-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-3.1.0.tgz", - "integrity": "sha512-tVI25WEXl4fckNEmrq70xU1XumxUwEx/FZD5AgEcV8ri7Wvrg2o7GEq8U7htrNx3CajciGm+kDyhRf5JB6t7/A==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", @@ -12087,15 +12054,6 @@ "dev": true, "license": "MIT" }, - "node_modules/is-reference": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", - "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", - "dev": true, - "dependencies": { - "@types/estree": "^1.0.6" - } - }, "node_modules/is-regex": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", @@ -14483,17 +14441,6 @@ "url": "https://ko-fi.com/killymxi" } }, - "node_modules/periscopic": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-4.0.2.tgz", - "integrity": "sha512-sqpQDUy8vgB7ycLkendSKS6HnVz1Rneoc3Rc+ZBUCe2pbqlVuCC5vF52l0NJ1aiMg/r1qfYF9/myz8CZeI2rjA==", - "dev": true, - "dependencies": { - "@types/estree": "*", - "is-reference": "^3.0.2", - "zimmerframe": "^1.0.0" - } - }, "node_modules/pg-int8": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", @@ -15451,9 +15398,10 @@ } }, "node_modules/react-router": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.8.1.tgz", - "integrity": "sha512-5cy/M8DHcG51/KUIka1nfZ2QeylS4PJRs6TT8I4PF5axVsI5JUxp0hC0NZ/AEEj8Vw7xsEoD7L/6FY+zoYaOGA==", + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.0.tgz", + "integrity": "sha512-gmmc2UNj8oS8Z2JGpfAmhLv+j5O9Xciv2HAGZN0rV//ycoe1E40xN3ovqLZD7PsMDkoJvsbASE8TjAY+Xm7DKQ==", + "license": "MIT", "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" @@ -18164,20 +18112,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vitefu": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", - "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", - "dev": true, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" - }, - "peerDependenciesMeta": { - "vite": { - "optional": true - } - } - }, "node_modules/vitest": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", @@ -18791,12 +18725,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/zimmerframe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz", - "integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==", - "dev": true - }, "node_modules/zod": { "version": "3.25.67", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.67.tgz", diff --git a/package.json b/package.json index ef4672bd7..8691b49e9 100644 --- a/package.json +++ b/package.json @@ -61,9 +61,9 @@ "@radix-ui/react-toast": "^1.2.11", "@radix-ui/react-tooltip": "^1.2.4", "@react-email/components": "0.0.38", - "@react-router/express": "^7.8.1", - "@react-router/node": "^7.8.1", - "@react-router/remix-routes-option-adapter": "^7.8.1", + "@react-router/express": "^7.9.0", + "@react-router/node": "^7.9.0", + "@react-router/remix-routes-option-adapter": "^7.9.0", "@remix-run/server-runtime": "^2.16.5", "@sentry/profiling-node": "^9.32.0", "@sentry/react-router": "^9.32.0", @@ -98,7 +98,7 @@ "qrcode": "^1.5.4", "react": "^19.1.0", "react-dom": "^19.1.0", - "react-router": "^7.8.1", + "react-router": "^7.9.0", "remix-auth": "^4.2.0", "remix-auth-github": "^3.0.2", "remix-utils": "^8.5.0", @@ -116,7 +116,7 @@ "@epic-web/config": "^1.20.1", "@faker-js/faker": "^9.7.0", "@playwright/test": "^1.52.0", - "@react-router/dev": "^7.8.1", + "@react-router/dev": "^7.9.0", "@sly-cli/sly": "^2.1.1", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/react-router.config.ts b/react-router.config.ts index 875073799..25a434013 100644 --- a/react-router.config.ts +++ b/react-router.config.ts @@ -11,6 +11,7 @@ export default { future: { unstable_optimizeDeps: true, + v8_middleware: true, }, buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => { diff --git a/server/index.ts b/server/index.ts index b55f22cf1..85f552f12 100644 --- a/server/index.ts +++ b/server/index.ts @@ -9,7 +9,7 @@ import express from 'express' import rateLimit from 'express-rate-limit' import getPort, { portNumbers } from 'get-port' import morgan from 'morgan' -import { type ServerBuild } from 'react-router' +import { RouterContextProvider, type ServerBuild, createContext } from 'react-router' const MODE = process.env.NODE_ENV ?? 'development' const IS_PROD = MODE === 'production' @@ -197,10 +197,16 @@ if (!ALLOW_INDEXING) { }) } +const serverBuildContext = createContext | null>(null) + app.all( '*', createRequestHandler({ - getLoadContext: () => ({ serverBuild: getBuild() }), + getLoadContext: () => { + const ctx = new RouterContextProvider() + ctx.set(serverBuildContext, getBuild()) + return ctx + }, mode: MODE, build: async () => { const { error, build } = await getBuild() From 05e00cf2d9b95b52ffe4f6cfbc41986e21bd8195 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Sep 2025 16:22:55 +0000 Subject: [PATCH 2/6] Checkpoint before follow-up message Co-authored-by: me --- app/routes/_auth+/login.server.ts | 3 +++ app/routes/_auth+/login.tsx | 3 --- app/routes/_auth+/signup.server.ts | 3 +++ app/routes/settings+/profile.server.ts | 3 +++ app/routes/settings+/profile.tsx | 4 ---- 5 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 app/routes/_auth+/signup.server.ts create mode 100644 app/routes/settings+/profile.server.ts diff --git a/app/routes/_auth+/login.server.ts b/app/routes/_auth+/login.server.ts index 9d5cf0bfb..4582f83cf 100644 --- a/app/routes/_auth+/login.server.ts +++ b/app/routes/_auth+/login.server.ts @@ -9,6 +9,9 @@ import { authSessionStorage } from '#app/utils/session.server.ts' import { redirectWithToast } from '#app/utils/toast.server.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' + +export const unstable_middleware = [requireAnonymousMiddleware] const verifiedTimeKey = 'verified-time' const unverifiedSessionIdKey = 'unverified-session-id' diff --git a/app/routes/_auth+/login.tsx b/app/routes/_auth+/login.tsx index 0e3ee139c..c60f272d8 100644 --- a/app/routes/_auth+/login.tsx +++ b/app/routes/_auth+/login.tsx @@ -12,9 +12,6 @@ import { Spacer } from '#app/components/spacer.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { login } from '#app/utils/auth.server.ts' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, -] import { ProviderConnectionForm, providerNames, diff --git a/app/routes/_auth+/signup.server.ts b/app/routes/_auth+/signup.server.ts new file mode 100644 index 000000000..e05741519 --- /dev/null +++ b/app/routes/_auth+/signup.server.ts @@ -0,0 +1,3 @@ +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' + +export const unstable_middleware = [requireAnonymousMiddleware] \ No newline at end of file diff --git a/app/routes/settings+/profile.server.ts b/app/routes/settings+/profile.server.ts new file mode 100644 index 000000000..4dfe4b90d --- /dev/null +++ b/app/routes/settings+/profile.server.ts @@ -0,0 +1,3 @@ +import { requireUserMiddleware } from '#app/middleware.server.ts' + +export const unstable_middleware = [requireUserMiddleware] \ No newline at end of file diff --git a/app/routes/settings+/profile.tsx b/app/routes/settings+/profile.tsx index 77afcbf59..8775e0c8c 100644 --- a/app/routes/settings+/profile.tsx +++ b/app/routes/settings+/profile.tsx @@ -10,10 +10,6 @@ import { cn } from '#app/utils/misc.tsx' import { useUser } from '#app/utils/user.ts' import { type Route } from './+types/profile.ts' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireUserMiddleware, -] - export const BreadcrumbHandle = z.object({ breadcrumb: z.any() }) export type BreadcrumbHandle = z.infer From 1e86901002b7ed70c80a29f0dc871585051c2275 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Sep 2025 16:24:45 +0000 Subject: [PATCH 3/6] Refactor: Apply requireAnonymousMiddleware to auth routes Co-authored-by: me --- app/routes/_auth+/onboarding.server.ts | 3 +++ .../_auth+/onboarding_.$provider.server.ts | 20 ++----------------- app/routes/_auth+/onboarding_.$provider.tsx | 3 --- app/routes/_auth+/reset-password.server.ts | 3 +++ app/routes/_auth+/signup.tsx | 3 --- 5 files changed, 8 insertions(+), 24 deletions(-) diff --git a/app/routes/_auth+/onboarding.server.ts b/app/routes/_auth+/onboarding.server.ts index 502ef8078..a0f271107 100644 --- a/app/routes/_auth+/onboarding.server.ts +++ b/app/routes/_auth+/onboarding.server.ts @@ -3,6 +3,9 @@ import { redirect } from 'react-router' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { onboardingEmailSessionKey } from './onboarding.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' + +export const unstable_middleware = [requireAnonymousMiddleware] export async function handleVerification({ submission }: VerifyFunctionArgs) { invariant( diff --git a/app/routes/_auth+/onboarding_.$provider.server.ts b/app/routes/_auth+/onboarding_.$provider.server.ts index 502ef8078..31f368ed0 100644 --- a/app/routes/_auth+/onboarding_.$provider.server.ts +++ b/app/routes/_auth+/onboarding_.$provider.server.ts @@ -1,19 +1,3 @@ -import { invariant } from '@epic-web/invariant' -import { redirect } from 'react-router' -import { verifySessionStorage } from '#app/utils/verification.server.ts' -import { onboardingEmailSessionKey } from './onboarding.tsx' -import { type VerifyFunctionArgs } from './verify.server.ts' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export async function handleVerification({ submission }: VerifyFunctionArgs) { - invariant( - submission.status === 'success', - 'Submission should be successful by now', - ) - const verifySession = await verifySessionStorage.getSession() - verifySession.set(onboardingEmailSessionKey, submission.value.target) - return redirect('/onboarding', { - headers: { - 'set-cookie': await verifySessionStorage.commitSession(verifySession), - }, - }) -} +export const unstable_middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_auth+/onboarding_.$provider.tsx b/app/routes/_auth+/onboarding_.$provider.tsx index bbe39452e..4975975be 100644 --- a/app/routes/_auth+/onboarding_.$provider.tsx +++ b/app/routes/_auth+/onboarding_.$provider.tsx @@ -18,9 +18,6 @@ import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { sessionKey, signupWithConnection } from '#app/utils/auth.server.ts' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, -] import { ProviderNameSchema } from '#app/utils/connections.tsx' import { prisma } from '#app/utils/db.server.ts' import { useIsPending } from '#app/utils/misc.tsx' diff --git a/app/routes/_auth+/reset-password.server.ts b/app/routes/_auth+/reset-password.server.ts index f5b2d5072..bb85470da 100644 --- a/app/routes/_auth+/reset-password.server.ts +++ b/app/routes/_auth+/reset-password.server.ts @@ -4,6 +4,9 @@ import { prisma } from '#app/utils/db.server.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { resetPasswordUsernameSessionKey } from './reset-password.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' + +export const unstable_middleware = [requireAnonymousMiddleware] export async function handleVerification({ submission }: VerifyFunctionArgs) { invariant( diff --git a/app/routes/_auth+/signup.tsx b/app/routes/_auth+/signup.tsx index f0f0164b0..55ed8910b 100644 --- a/app/routes/_auth+/signup.tsx +++ b/app/routes/_auth+/signup.tsx @@ -8,9 +8,6 @@ import { z } from 'zod' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, -] import { ProviderConnectionForm, providerNames, From d0db7c619763473ffbd0a6e47164422ac8c39928 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Sep 2025 16:29:49 +0000 Subject: [PATCH 4/6] Refactor: Use `middleware` instead of `unstable_middleware` Refactor: Use `prisma.$queryRawTyped` for user search Co-authored-by: me --- app/routes/_auth+/login.server.ts | 2 +- app/routes/_auth+/onboarding.server.ts | 2 +- app/routes/_auth+/onboarding.tsx | 3 --- app/routes/_auth+/onboarding_.$provider.server.ts | 2 +- app/routes/_auth+/reset-password.server.ts | 2 +- app/routes/_auth+/reset-password.tsx | 3 --- app/routes/_auth+/signup.server.ts | 2 +- app/routes/settings+/profile.server.ts | 2 +- app/routes/users+/index.tsx | 9 +++------ 9 files changed, 9 insertions(+), 18 deletions(-) diff --git a/app/routes/_auth+/login.server.ts b/app/routes/_auth+/login.server.ts index 4582f83cf..cc774a95f 100644 --- a/app/routes/_auth+/login.server.ts +++ b/app/routes/_auth+/login.server.ts @@ -11,7 +11,7 @@ import { verifySessionStorage } from '#app/utils/verification.server.ts' import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts' import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireAnonymousMiddleware] +export const middleware = [requireAnonymousMiddleware] const verifiedTimeKey = 'verified-time' const unverifiedSessionIdKey = 'unverified-session-id' diff --git a/app/routes/_auth+/onboarding.server.ts b/app/routes/_auth+/onboarding.server.ts index a0f271107..9efba16b9 100644 --- a/app/routes/_auth+/onboarding.server.ts +++ b/app/routes/_auth+/onboarding.server.ts @@ -5,7 +5,7 @@ import { onboardingEmailSessionKey } from './onboarding.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireAnonymousMiddleware] +export const middleware = [requireAnonymousMiddleware] export async function handleVerification({ submission }: VerifyFunctionArgs) { invariant( diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index 70fb3bd1b..63aa7423d 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -8,9 +8,6 @@ import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { checkIsCommonPassword, sessionKey, signup } from '#app/utils/auth.server.ts' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, -] import { prisma } from '#app/utils/db.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' import { useIsPending } from '#app/utils/misc.tsx' diff --git a/app/routes/_auth+/onboarding_.$provider.server.ts b/app/routes/_auth+/onboarding_.$provider.server.ts index 31f368ed0..faec312f1 100644 --- a/app/routes/_auth+/onboarding_.$provider.server.ts +++ b/app/routes/_auth+/onboarding_.$provider.server.ts @@ -1,3 +1,3 @@ import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireAnonymousMiddleware] +export const middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_auth+/reset-password.server.ts b/app/routes/_auth+/reset-password.server.ts index bb85470da..d76649330 100644 --- a/app/routes/_auth+/reset-password.server.ts +++ b/app/routes/_auth+/reset-password.server.ts @@ -6,7 +6,7 @@ import { resetPasswordUsernameSessionKey } from './reset-password.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireAnonymousMiddleware] +export const middleware = [requireAnonymousMiddleware] export async function handleVerification({ submission }: VerifyFunctionArgs) { invariant( diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index 12c3b3a28..4daffa981 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -6,9 +6,6 @@ import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' import { checkIsCommonPassword, resetUserPassword } from '#app/utils/auth.server.ts' -export const unstable_middleware = [ - (await import('#app/middleware.server.ts')).requireAnonymousMiddleware, -] import { useIsPending } from '#app/utils/misc.tsx' import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' diff --git a/app/routes/_auth+/signup.server.ts b/app/routes/_auth+/signup.server.ts index e05741519..1eb579b9a 100644 --- a/app/routes/_auth+/signup.server.ts +++ b/app/routes/_auth+/signup.server.ts @@ -1,3 +1,3 @@ import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireAnonymousMiddleware] \ No newline at end of file +export const middleware = [requireAnonymousMiddleware] \ No newline at end of file diff --git a/app/routes/settings+/profile.server.ts b/app/routes/settings+/profile.server.ts index 4dfe4b90d..5f7f897e3 100644 --- a/app/routes/settings+/profile.server.ts +++ b/app/routes/settings+/profile.server.ts @@ -1,3 +1,3 @@ import { requireUserMiddleware } from '#app/middleware.server.ts' -export const unstable_middleware = [requireUserMiddleware] \ No newline at end of file +export const middleware = [requireUserMiddleware] \ No newline at end of file diff --git a/app/routes/users+/index.tsx b/app/routes/users+/index.tsx index edc1ac45c..abc58c037 100644 --- a/app/routes/users+/index.tsx +++ b/app/routes/users+/index.tsx @@ -1,4 +1,4 @@ -// using $queryRawUnsafe for LIKE query construction +import { searchUsers } from '@prisma/client/sql' import { Img } from 'openimg/react' import { redirect, Link } from 'react-router' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' @@ -15,10 +15,7 @@ export async function loader({ request }: Route.LoaderArgs) { } const like = `%${searchTerm ?? ''}%` - const users = await prisma.$queryRawUnsafe<{ id: string; username: string; name: string | null; imageObjectKey: string | null }[]>( - `SELECT id, username, name, (SELECT "objectKey" FROM "Image" WHERE "userId" = "User"."id" LIMIT 1) as "imageObjectKey" FROM "User" WHERE username ILIKE $1 OR name ILIKE $1 ORDER BY username ASC`, - like, - ) + const users = await prisma.$queryRawTyped(searchUsers(like)) return { status: 'idle', users } as const } @@ -52,7 +49,7 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) { > {user.name Date: Fri, 12 Sep 2025 17:02:35 +0000 Subject: [PATCH 5/6] Refactor: Improve middleware and context usage This commit refactors middleware and context usage for better code organization and clarity. It also includes minor updates to documentation and tests. Co-authored-by: me --- app/context.ts | 1 - app/middleware.server.ts | 46 ++++++------ .../_auth+/auth.$provider.callback.test.ts | 54 ++++++++++---- app/routes/_auth+/login.server.ts | 2 +- app/routes/_auth+/onboarding.server.ts | 2 +- app/routes/_auth+/onboarding.tsx | 6 +- app/routes/_auth+/reset-password.server.ts | 2 +- app/routes/_auth+/reset-password.tsx | 5 +- app/routes/_auth+/signup.server.ts | 2 +- app/routes/_seo+/sitemap[.]xml.ts | 15 +++- app/routes/settings+/profile.server.ts | 2 +- app/routes/users+/index.tsx | 2 +- app/utils/cache.server.ts | 2 +- docs/database.md | 1 - docs/decisions/039-passkeys.md | 12 ---- docs/decisions/043-pwnedpasswords.md | 3 - docs/decisions/044-rr-devtools.md | 68 +++++++++--------- prisma/dev.db | Bin 0 -> 184320 bytes server/index.ts | 13 +++- tests/e2e/search.test.ts | 8 +-- 20 files changed, 140 insertions(+), 106 deletions(-) create mode 100644 prisma/dev.db diff --git a/app/context.ts b/app/context.ts index 7b5725d5b..ead665b34 100644 --- a/app/context.ts +++ b/app/context.ts @@ -2,4 +2,3 @@ import { createContext } from 'react-router' // Holds the authenticated user's ID for routes protected by middleware export const userIdContext = createContext(null) - diff --git a/app/middleware.server.ts b/app/middleware.server.ts index 2f3e8c6f7..a4f473751 100644 --- a/app/middleware.server.ts +++ b/app/middleware.server.ts @@ -1,35 +1,39 @@ import { redirect, type MiddlewareFunction } from 'react-router' +import { userIdContext } from '#app/context.ts' import { prisma } from '#app/utils/db.server.ts' import { authSessionStorage } from '#app/utils/session.server.ts' -import { userIdContext } from '#app/context.ts' export const requireUserMiddleware: MiddlewareFunction = async ({ - request, - context, + request, + context, }) => { - const cookie = request.headers.get('cookie') - const session = await authSessionStorage.getSession(cookie) - const sessionId = session.get('sessionId') as string | undefined - if (!sessionId) throw redirect(`/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`) + const cookie = request.headers.get('cookie') + const session = await authSessionStorage.getSession(cookie) + const sessionId = session.get('sessionId') as string | undefined + if (!sessionId) + throw redirect( + `/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`, + ) - const sessionRecord = await prisma.session.findUnique({ - select: { userId: true, expirationDate: true }, - where: { id: sessionId }, - }) + const sessionRecord = await prisma.session.findUnique({ + select: { userId: true, expirationDate: true }, + where: { id: sessionId }, + }) - if (!sessionRecord || sessionRecord.expirationDate < new Date()) { - throw redirect(`/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`) - } + if (!sessionRecord || sessionRecord.expirationDate < new Date()) { + throw redirect( + `/login?redirectTo=${encodeURIComponent(new URL(request.url).pathname)}`, + ) + } - context.set(userIdContext, sessionRecord.userId) + context.set(userIdContext, sessionRecord.userId) } export const requireAnonymousMiddleware: MiddlewareFunction = async ({ - request, + request, }) => { - const cookie = request.headers.get('cookie') - const session = await authSessionStorage.getSession(cookie) - const sessionId = session.get('sessionId') as string | undefined - if (sessionId) throw redirect('/') + const cookie = request.headers.get('cookie') + const session = await authSessionStorage.getSession(cookie) + const sessionId = session.get('sessionId') as string | undefined + if (sessionId) throw redirect('/') } - diff --git a/app/routes/_auth+/auth.$provider.callback.test.ts b/app/routes/_auth+/auth.$provider.callback.test.ts index 7a436fa2c..c160e7b96 100644 --- a/app/routes/_auth+/auth.$provider.callback.test.ts +++ b/app/routes/_auth+/auth.$provider.callback.test.ts @@ -2,8 +2,8 @@ import { invariant } from '@epic-web/invariant' import { faker } from '@faker-js/faker' import { SetCookie } from '@mjackson/headers' import { http } from 'msw' -import { afterEach, expect, test } from 'vitest' import { RouterContextProvider } from 'react-router' +import { afterEach, expect, test } from 'vitest' import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' import { getSessionExpirationDate, sessionKey } from '#app/utils/auth.server.ts' import { GITHUB_PROVIDER_NAME } from '#app/utils/connections.tsx' @@ -26,9 +26,11 @@ afterEach(async () => { test('a new user goes to onboarding', async () => { const request = await setupRequest() - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }).catch( - (e) => e, - ) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }).catch((e) => e) expect(response).toHaveRedirect('/onboarding/github') }) @@ -40,9 +42,11 @@ test('when auth fails, send the user to login with a toast', async () => { }), ) const request = await setupRequest() - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }).catch( - (e) => e, - ) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }).catch((e) => e) invariant(response instanceof Response, 'response should be a Response') expect(response).toHaveRedirect('/login') await expect(response).toSendToast( @@ -61,7 +65,11 @@ test('when a user is logged in, it creates the connection', async () => { sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -97,7 +105,11 @@ test(`when a user is logged in and has already connected, it doesn't do anything sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -112,7 +124,11 @@ test('when a user exists with the same email, create connection and make session const email = githubUser.primaryEmail.toLowerCase() const { userId } = await setupUser({ ...createUser(), email }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) expect(response).toHaveRedirect('/') @@ -156,7 +172,11 @@ test('gives an error if the account is already connected to another user', async sessionId: session.id, code: githubUser.code, }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) expect(response).toHaveRedirect('/settings/profile/connections') await expect(response).toSendToast( expect.objectContaining({ @@ -179,7 +199,11 @@ test('if a user is not logged in, but the connection exists, make a session', as }, }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) expect(response).toHaveRedirect('/') await expect(response).toHaveSessionForUser(userId) }) @@ -203,7 +227,11 @@ test('if a user is not logged in, but the connection exists and they have enable }, }) const request = await setupRequest({ code: githubUser.code }) - const response = await loader({ request, params: PARAMS, context: new RouterContextProvider() as any }) + const response = await loader({ + request, + params: PARAMS, + context: new RouterContextProvider() as any, + }) const searchParams = new URLSearchParams({ type: twoFAVerificationType, target: userId, diff --git a/app/routes/_auth+/login.server.ts b/app/routes/_auth+/login.server.ts index cc774a95f..b27f494c5 100644 --- a/app/routes/_auth+/login.server.ts +++ b/app/routes/_auth+/login.server.ts @@ -1,6 +1,7 @@ import { invariant } from '@epic-web/invariant' import { redirect } from 'react-router' import { safeRedirect } from 'remix-utils/safe-redirect' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' import { twoFAVerificationType } from '#app/routes/settings+/profile.two-factor.tsx' import { getUserId, sessionKey } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' @@ -9,7 +10,6 @@ import { authSessionStorage } from '#app/utils/session.server.ts' import { redirectWithToast } from '#app/utils/toast.server.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { getRedirectToUrl, type VerifyFunctionArgs } from './verify.server.ts' -import { requireAnonymousMiddleware } from '#app/middleware.server.ts' export const middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_auth+/onboarding.server.ts b/app/routes/_auth+/onboarding.server.ts index 9efba16b9..d6b547cbd 100644 --- a/app/routes/_auth+/onboarding.server.ts +++ b/app/routes/_auth+/onboarding.server.ts @@ -1,9 +1,9 @@ import { invariant } from '@epic-web/invariant' import { redirect } from 'react-router' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { onboardingEmailSessionKey } from './onboarding.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' -import { requireAnonymousMiddleware } from '#app/middleware.server.ts' export const middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_auth+/onboarding.tsx b/app/routes/_auth+/onboarding.tsx index 63aa7423d..227a109fa 100644 --- a/app/routes/_auth+/onboarding.tsx +++ b/app/routes/_auth+/onboarding.tsx @@ -7,7 +7,11 @@ import { z } from 'zod' import { CheckboxField, ErrorList, Field } from '#app/components/forms.tsx' import { Spacer } from '#app/components/spacer.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { checkIsCommonPassword, sessionKey, signup } from '#app/utils/auth.server.ts' +import { + checkIsCommonPassword, + sessionKey, + signup, +} from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { checkHoneypot } from '#app/utils/honeypot.server.ts' import { useIsPending } from '#app/utils/misc.tsx' diff --git a/app/routes/_auth+/reset-password.server.ts b/app/routes/_auth+/reset-password.server.ts index d76649330..a820a7872 100644 --- a/app/routes/_auth+/reset-password.server.ts +++ b/app/routes/_auth+/reset-password.server.ts @@ -1,10 +1,10 @@ import { invariant } from '@epic-web/invariant' import { data, redirect } from 'react-router' +import { requireAnonymousMiddleware } from '#app/middleware.server.ts' import { prisma } from '#app/utils/db.server.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' import { resetPasswordUsernameSessionKey } from './reset-password.tsx' import { type VerifyFunctionArgs } from './verify.server.ts' -import { requireAnonymousMiddleware } from '#app/middleware.server.ts' export const middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_auth+/reset-password.tsx b/app/routes/_auth+/reset-password.tsx index 4daffa981..023f3d469 100644 --- a/app/routes/_auth+/reset-password.tsx +++ b/app/routes/_auth+/reset-password.tsx @@ -5,7 +5,10 @@ import { data, redirect, Form } from 'react-router' import { GeneralErrorBoundary } from '#app/components/error-boundary.tsx' import { ErrorList, Field } from '#app/components/forms.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { checkIsCommonPassword, resetUserPassword } from '#app/utils/auth.server.ts' +import { + checkIsCommonPassword, + resetUserPassword, +} from '#app/utils/auth.server.ts' import { useIsPending } from '#app/utils/misc.tsx' import { PasswordAndConfirmPasswordSchema } from '#app/utils/user-validation.ts' import { verifySessionStorage } from '#app/utils/verification.server.ts' diff --git a/app/routes/_auth+/signup.server.ts b/app/routes/_auth+/signup.server.ts index 1eb579b9a..faec312f1 100644 --- a/app/routes/_auth+/signup.server.ts +++ b/app/routes/_auth+/signup.server.ts @@ -1,3 +1,3 @@ import { requireAnonymousMiddleware } from '#app/middleware.server.ts' -export const middleware = [requireAnonymousMiddleware] \ No newline at end of file +export const middleware = [requireAnonymousMiddleware] diff --git a/app/routes/_seo+/sitemap[.]xml.ts b/app/routes/_seo+/sitemap[.]xml.ts index e54f2ffca..0f40278a7 100644 --- a/app/routes/_seo+/sitemap[.]xml.ts +++ b/app/routes/_seo+/sitemap[.]xml.ts @@ -1,12 +1,21 @@ import { generateSitemap } from '@nasa-gcn/remix-seo' -import { type ServerBuild, RouterContextProvider, createContext } from 'react-router' +import { + type ServerBuild, + type RouterContextProvider, + createContext, +} from 'react-router' import { getDomainUrl } from '#app/utils/misc.tsx' import { type Route } from './+types/sitemap[.]xml.ts' // recreate context key to match the one set in server getLoadContext -export const serverBuildContext = createContext | null>(null) +export const serverBuildContext = createContext | null>(null) export async function loader({ request, context }: Route.LoaderArgs) { - const serverBuild = (await (context as Readonly).get(serverBuildContext)) as { build: ServerBuild } + const serverBuild = (await (context as Readonly).get( + serverBuildContext, + )) as { build: ServerBuild } // TODO: This is typeerror is coming up since of the remix-run/server-runtime package. We might need to remove/update that one. // @ts-expect-error diff --git a/app/routes/settings+/profile.server.ts b/app/routes/settings+/profile.server.ts index 5f7f897e3..64ce05a2a 100644 --- a/app/routes/settings+/profile.server.ts +++ b/app/routes/settings+/profile.server.ts @@ -1,3 +1,3 @@ import { requireUserMiddleware } from '#app/middleware.server.ts' -export const middleware = [requireUserMiddleware] \ No newline at end of file +export const middleware = [requireUserMiddleware] diff --git a/app/routes/users+/index.tsx b/app/routes/users+/index.tsx index abc58c037..d6a5e1221 100644 --- a/app/routes/users+/index.tsx +++ b/app/routes/users+/index.tsx @@ -70,7 +70,7 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) {

No users found

) ) : loaderData.status === 'error' ? ( - + ) : null} diff --git a/app/utils/cache.server.ts b/app/utils/cache.server.ts index b10f2b631..85d52b459 100644 --- a/app/utils/cache.server.ts +++ b/app/utils/cache.server.ts @@ -1,5 +1,6 @@ import fs from 'node:fs' import path from 'node:path' +import { DatabaseSync } from 'node:sqlite' import { cachified as baseCachified, verboseReporter, @@ -13,7 +14,6 @@ import { } from '@epic-web/cachified' import { remember } from '@epic-web/remember' import { LRUCache } from 'lru-cache' -import { DatabaseSync } from 'node:sqlite' import { z } from 'zod' import { updatePrimaryCacheValue } from '#app/routes/admin+/cache_.sqlite.server.ts' import { getInstanceInfo, getInstanceInfoSync } from './litefs.server.ts' diff --git a/docs/database.md b/docs/database.md index 746714d37..f9d69c322 100644 --- a/docs/database.md +++ b/docs/database.md @@ -300,7 +300,6 @@ You've got a few options: re-generating the migration after fixing the error. 3. If you do care about the data and don't have a backup, you can follow these steps: - 1. Comment out the [`exec` section from `litefs.yml` file](https://github.com/epicweb-dev/epic-stack/blob/main/other/litefs.yml#L31-L37). diff --git a/docs/decisions/039-passkeys.md b/docs/decisions/039-passkeys.md index af37ca62e..32916b52f 100644 --- a/docs/decisions/039-passkeys.md +++ b/docs/decisions/039-passkeys.md @@ -11,7 +11,6 @@ username/password and OAuth providers. While these methods are widely used, they come with various security challenges: 1. Password-based authentication: - - Users often reuse passwords across services - Passwords can be phished or stolen - Password management is a burden for users @@ -39,14 +38,12 @@ using: The authentication flow works as follows: 1. Registration: - - Server generates a challenge and sends registration options - Client creates a new key pair and signs the challenge with the private key - Public key and metadata are sent to the server for storage - Private key remains securely stored in the authenticator 2. Authentication: - - Server generates a new challenge - Client signs it with the stored private key - Server verifies the signature using the stored public key @@ -64,19 +61,16 @@ While passkeys represent the future of authentication, we maintain support for password and OAuth authentication because: 1. Adoption and Transition: - - Passkey support is still rolling out across platforms and browsers - Users need time to become comfortable with the new technology - Organizations may have existing requirements for specific auth methods 2. Fallback Options: - - Some users may not have compatible devices - Enterprise environments might restrict biometric authentication - Backup authentication methods provide reliability 3. User Choice: - - Different users have different security/convenience preferences - Some scenarios may require specific authentication types - Supporting multiple methods maximizes accessibility @@ -112,20 +106,17 @@ We chose SimpleWebAuthn because: ### Positive: 1. Enhanced Security for Users: - - Phishing-resistant authentication adds protection against common attacks - Hardware-backed security provides stronger guarantees than passwords alone - Biometric authentication reduces risk of credential sharing 2. Improved User Experience Options: - - Users can choose between password, OAuth, or passkey based on their needs - Native biometric flows provide fast and familiar authentication - Password manager integration enables seamless cross-device access - Multiple authentication methods increase accessibility 3. Future-Proofing Authentication: - - Adoption of web standard - Gradual transition path as passkey support grows - Meeting evolving security best practices @@ -133,14 +124,12 @@ We chose SimpleWebAuthn because: ### Negative: 1. Implementation Complexity: - - WebAuthn is a complex specification - Need to handle various device capabilities - Must maintain backward compatibility - Need to maintain password-based auth as fallback 2. User Education: - - New technology requires user education - Some users may be hesitant to adopt - Need clear documentation and UI guidance @@ -148,7 +137,6 @@ We chose SimpleWebAuthn because: ### Neutral: 1. Data Storage: - - New database model for passkeys - Additional storage requirements per user - Migration path for existing users diff --git a/docs/decisions/043-pwnedpasswords.md b/docs/decisions/043-pwnedpasswords.md index 366105e6f..e457f537d 100644 --- a/docs/decisions/043-pwnedpasswords.md +++ b/docs/decisions/043-pwnedpasswords.md @@ -22,14 +22,12 @@ However, we wanted to implement this in a way that: We will integrate the HaveIBeenPwned Password API with the following approach: 1. **Progressive Enhancement** - - The password check is implemented as a non-blocking enhancement - If the check fails or times out (>1s), we allow the password - This ensures users can still set passwords even if the service is unavailable 2. **Development Experience** - - The API calls are mocked during development and testing using MSW (Mock Service Worker) - This prevents unnecessary API calls during development @@ -37,7 +35,6 @@ We will integrate the HaveIBeenPwned Password API with the following approach: - Follows our pattern of mocking external services 3. **Error Handling** - - Timeout after 1 second to prevent blocking users - Graceful fallback if the service is unavailable - Warning logs for monitoring service health diff --git a/docs/decisions/044-rr-devtools.md b/docs/decisions/044-rr-devtools.md index 904051aba..60b4c52b4 100644 --- a/docs/decisions/044-rr-devtools.md +++ b/docs/decisions/044-rr-devtools.md @@ -6,34 +6,33 @@ Status: accepted ## Context -Epic Stack uses React Router for routing. React Router is a powerful -library, but it can be difficult to debug and visualize the routing -in your application. This is especially true when you have a complex -routing structure with nested routes, dynamic routes, and you rely -on data functions like loaders and actions, which the Epic Stack does. - -It is also hard to know which routes are currently active -(which ones are rendered) and if any if the loaders are triggered -when you expect them to be. This can lead to confusion and frustration -and the use of console.log statements to debug the routing in your -application. - -This is where the React Router DevTools come in. The React -Router DevTools are a set of tools that do all of these things for you. - -React Router has a set of DevTools that help debug and visualize the -routing in your application. The DevTools allow you to see the -current route information, including the current location, the matched -routes, and the route hierarchy. This can be very helpful when debugging -your applications. The DevTools also hook into your server-side by -wrapping loaders and actions, allowing you to get extensive -information about the data being loaded and the actions being dispatched. +Epic Stack uses React Router for routing. React Router is a powerful library, +but it can be difficult to debug and visualize the routing in your application. +This is especially true when you have a complex routing structure with nested +routes, dynamic routes, and you rely on data functions like loaders and actions, +which the Epic Stack does. + +It is also hard to know which routes are currently active (which ones are +rendered) and if any if the loaders are triggered when you expect them to be. +This can lead to confusion and frustration and the use of console.log statements +to debug the routing in your application. + +This is where the React Router DevTools come in. The React Router DevTools are a +set of tools that do all of these things for you. + +React Router has a set of DevTools that help debug and visualize the routing in +your application. The DevTools allow you to see the current route information, +including the current location, the matched routes, and the route hierarchy. +This can be very helpful when debugging your applications. The DevTools also +hook into your server-side by wrapping loaders and actions, allowing you to get +extensive information about the data being loaded and the actions being +dispatched. ## Decision -We will add the React Router DevTools to the Epic Stack. The DevTools -will be added to the project as a development dependency. The DevTools -will be used in development mode only. +We will add the React Router DevTools to the Epic Stack. The DevTools will be +added to the project as a development dependency. The DevTools will be used in +development mode only. The DevTools will be used to enhance the following: @@ -45,19 +44,18 @@ The DevTools will be used to enhance the following: 6. See cache information returned via headers from your loaders 7. See which loaders/actions are triggered when you navigate to a route 8. and a lot more! - ## Consequences With the addition of the React Router DevTools, you will not have to rely on -console.log statements to debug your routing. The DevTools will provide you -with the tools to ship your applications faster and with more confidence. +console.log statements to debug your routing. The DevTools will provide you with +the tools to ship your applications faster and with more confidence. -The DevTools will also help you visualize the routing in your application, -which can be very helpful in understanding routing in general, and figuring -out if your routes are set up correctly. +The DevTools will also help you visualize the routing in your application, which +can be very helpful in understanding routing in general, and figuring out if +your routes are set up correctly. -They are not included in the production build by default, so you will not -have to worry about them being included in your production bundle. -They are only included in development mode, so you can use them without -any negative performance impact in production. \ No newline at end of file +They are not included in the production build by default, so you will not have +to worry about them being included in your production bundle. They are only +included in development mode, so you can use them without any negative +performance impact in production. diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000000000000000000000000000000000000..262f76afbf8ff80b935cd517da15078aff9a9d3a GIT binary patch literal 184320 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 literal 0 HcmV?d00001 diff --git a/server/index.ts b/server/index.ts index 85f552f12..b933907e7 100644 --- a/server/index.ts +++ b/server/index.ts @@ -9,7 +9,11 @@ import express from 'express' import rateLimit from 'express-rate-limit' import getPort, { portNumbers } from 'get-port' import morgan from 'morgan' -import { RouterContextProvider, type ServerBuild, createContext } from 'react-router' +import { + RouterContextProvider, + type ServerBuild, + createContext, +} from 'react-router' const MODE = process.env.NODE_ENV ?? 'development' const IS_PROD = MODE === 'production' @@ -28,7 +32,7 @@ const viteDevServer = IS_PROD server: { middlewareMode: true, }, - // We tell Vite we are running a custom app instead of + // We tell Vite we are running a custom app instead of // the SPA default so it doesn't run HTML middleware appType: 'custom', }), @@ -197,7 +201,10 @@ if (!ALLOW_INDEXING) { }) } -const serverBuildContext = createContext | null>(null) +const serverBuildContext = createContext | null>(null) app.all( '*', diff --git a/tests/e2e/search.test.ts b/tests/e2e/search.test.ts index 35b23c91f..6efbd95aa 100644 --- a/tests/e2e/search.test.ts +++ b/tests/e2e/search.test.ts @@ -12,11 +12,9 @@ test('Search from home page', async ({ page, navigate, insertNewUser }) => { const userList = page.getByRole('main').getByRole('list') await expect(userList.getByRole('listitem')).toHaveCount(1) await expect( - userList - .getByRole('listitem') - .getByRole('link', { - name: `${newUser.name || newUser.username} profile`, - }), + userList.getByRole('listitem').getByRole('link', { + name: `${newUser.name || newUser.username} profile`, + }), ).toBeVisible() // Search for a non-existing user. From 426b315faa01b1e8c780f847caa82bcd84f869b7 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 12 Sep 2025 19:21:27 +0000 Subject: [PATCH 6/6] Extract router context to separate file Co-authored-by: me --- server/index.ts | 6 +----- server/router-context.ts | 7 +++++++ 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 server/router-context.ts diff --git a/server/index.ts b/server/index.ts index b933907e7..13c39c847 100644 --- a/server/index.ts +++ b/server/index.ts @@ -14,6 +14,7 @@ import { type ServerBuild, createContext, } from 'react-router' +import { serverBuildContext } from './router-context' const MODE = process.env.NODE_ENV ?? 'development' const IS_PROD = MODE === 'production' @@ -201,11 +202,6 @@ if (!ALLOW_INDEXING) { }) } -const serverBuildContext = createContext | null>(null) - app.all( '*', createRequestHandler({ diff --git a/server/router-context.ts b/server/router-context.ts new file mode 100644 index 000000000..bec9463d4 --- /dev/null +++ b/server/router-context.ts @@ -0,0 +1,7 @@ +import { createContext, type ServerBuild } from 'react-router' + +// Shared context key for passing the built routes into loaders (e.g. sitemap) +export const serverBuildContext = createContext< + Promise<{ error: unknown; build: ServerBuild }> | null +>(null) +