Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions app/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createContext } from 'react-router'

// Holds the authenticated user's ID for routes protected by middleware
export const userIdContext = createContext<string | null>(null)

35 changes: 35 additions & 0 deletions app/middleware.server.ts
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 4 in app/middleware.server.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`#app/context.ts` import should occur before import of `#app/utils/db.server.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('/')
}

17 changes: 9 additions & 8 deletions app/routes/_auth+/auth.$provider.callback.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { SetCookie } from '@mjackson/headers'
import { http } from 'msw'
import { afterEach, expect, test } from 'vitest'
import { RouterContextProvider } from 'react-router'

Check warning on line 6 in app/routes/_auth+/auth.$provider.callback.test.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

`react-router` import should occur before import of `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'
Expand All @@ -25,7 +26,7 @@

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')
Expand All @@ -39,7 +40,7 @@
}),
)
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')
Expand All @@ -60,7 +61,7 @@
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({
Expand Down Expand Up @@ -96,7 +97,7 @@
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({
Expand All @@ -111,7 +112,7 @@
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('/')

Expand Down Expand Up @@ -155,7 +156,7 @@
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({
Expand All @@ -178,7 +179,7 @@
},
})
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)
})
Expand All @@ -202,7 +203,7 @@
},
})
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,
Expand Down
7 changes: 4 additions & 3 deletions app/routes/_auth+/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
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 = [
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursor, I don't think this is unstable. Should we remove the prefix?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Working on it! I'll start making changes in this branch.

Open in Cursor Open in Web

(await import('#app/middleware.server.ts')).requireAnonymousMiddleware,
]
import {
ProviderConnectionForm,
providerNames,
Expand All @@ -37,13 +40,11 @@
options: z.object({ challenge: z.string() }),
}) satisfies z.ZodType<{ options: PublicKeyCredentialRequestOptionsJSON }>

export async function loader({ request }: Route.LoaderArgs) {

Check warning on line 43 in app/routes/_auth+/login.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'request' is defined but never used. Allowed unused args must match /^_/u
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, {
Expand Down
11 changes: 4 additions & 7 deletions app/routes/_auth+/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'),
)
Expand Down
10 changes: 4 additions & 6 deletions app/routes/_auth+/onboarding_.$provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -53,7 +52,6 @@ async function requireData({
request: Request
params: Params
}) {
await requireAnonymous(request)
const verifySession = await verifySessionStorage.getSession(
request.headers.get('cookie'),
)
Expand Down
10 changes: 4 additions & 6 deletions app/routes/_auth+/reset-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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'),
)
Expand Down
5 changes: 3 additions & 2 deletions app/routes/_auth+/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
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,
Expand All @@ -29,8 +31,7 @@
email: EmailSchema,
})

export async function loader({ request }: Route.LoaderArgs) {

Check warning on line 34 in app/routes/_auth+/signup.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

'request' is defined but never used. Allowed unused args must match /^_/u
await requireAnonymous(request)
return null
}

Expand Down
6 changes: 4 additions & 2 deletions app/routes/_seo+/sitemap[.]xml.ts
Original file line number Diff line number Diff line change
@@ -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'

Check warning on line 2 in app/routes/_seo+/sitemap[.]xml.ts

View workflow job for this annotation

GitHub Actions / ⬣ ESLint

Imports "RouterContextProvider" are only used as type
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<Promise<{ error: unknown; build: ServerBuild }> | null>(null)

export async function loader({ request, context }: Route.LoaderArgs) {
const serverBuild = (await context.serverBuild) as { build: ServerBuild }
const serverBuild = (await (context as Readonly<RouterContextProvider>).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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand Down
13 changes: 9 additions & 4 deletions app/routes/settings+/profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof BreadcrumbHandle>

Expand All @@ -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 })
Expand Down
11 changes: 7 additions & 4 deletions app/routes/users+/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cursor, don't do this. Instead, run the setup script which will generate the prisma typed sql. This change should be unnecessary once you do that.

Copy link

@cursor cursor bot Sep 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I updated the application to align with recent framework changes by migrating middleware exports from unstable_middleware to middleware across several route modules. Additionally, I enhanced data access safety by replacing an unsafe raw SQL query with a typed Prisma SQL function for user searches.

  • Middleware API Update: I updated the middleware export name from unstable_middleware to middleware in several server-side route modules, including app/routes/_auth+/login.server.ts, app/routes/_auth+/onboarding.server.ts, app/routes/_auth+/onboarding_.$provider.server.ts, app/routes/_auth+/reset-password.server.ts, app/routes/_auth+/signup.server.ts, and app/routes/settings+/profile.server.ts.
  • Removed Client-Side Middleware: I removed the unstable_middleware export entirely from client-side route modules app/routes/_auth+/onboarding.tsx and app/routes/_auth+/reset-password.tsx, as middleware is typically a server-side concept.
  • Typed Prisma SQL Query: In app/routes/users+/index.tsx, I replaced the prisma.$queryRawUnsafe call for searching users with a type-safe prisma.$queryRawTyped(searchUsers(like)). I also added an import for searchUsers from @prisma/client/sql to leverage this new typed query functionality, improving query safety and maintainability.
  • Image Source Type Refinement: In app/routes/users+/index.tsx, I simplified the getUserImgSrc call by removing the ?? undefined fallback for user.imageObjectKey, as the type system now correctly handles null values for the image object key.

Open in Cursor Open in Web

Learn more about Cursor Agents

return { status: 'idle', users } as const
}

Expand Down Expand Up @@ -49,7 +52,7 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) {
>
<Img
alt={user.name ?? user.username}
src={getUserImgSrc(user.imageObjectKey)}
src={getUserImgSrc(user.imageObjectKey ?? undefined)}
className="size-16 rounded-full"
width={256}
height={256}
Expand All @@ -70,7 +73,7 @@ export default function UsersRoute({ loaderData }: Route.ComponentProps) {
<p>No users found</p>
)
) : loaderData.status === 'error' ? (
<ErrorList errors={['There was an error parsing the results']} />
<ErrorList errors={["There was an error parsing the results"]} />
) : null}
</main>
</div>
Expand Down
Loading
Loading