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) {
>
No 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) {
>
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)
+