Skip to content

feat: migrate to remix-run/auth-middleware#132

Merged
mcansh merged 11 commits into
mainfrom
logan/auth-middleware
Apr 4, 2026
Merged

feat: migrate to remix-run/auth-middleware#132
mcansh merged 11 commits into
mainfrom
logan/auth-middleware

Conversation

@mcansh
Copy link
Copy Markdown
Owner

@mcansh mcansh commented Apr 4, 2026

  • feat: migrate to remix auth middleware
  • chore: remove unused login schema and user retrieval by ID
  • fix: comparison logic in password provider

Summary by CodeRabbit

  • Bug Fixes

    • More consistent sign-in errors and safe redirects back to the login page on failure.
    • Session handling improved on logout (auth cleared and session ID regenerated).
  • Refactor

    • Authentication flow and middleware reworked for more reliable access checks and consistent session state.
    • Redirect/return-to handling standardized across login, registration, and protected pages.

mcansh added 3 commits April 4, 2026 00:11
Signed-off-by: Logan McAnsh <logan@mcan.sh>
Signed-off-by: Logan McAnsh <logan@mcan.sh>
Signed-off-by: Logan McAnsh <logan@mcan.sh>
Copilot AI review requested due to automatic review settings April 4, 2026 04:26
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 4, 2026

⚠️ No Changeset found

Latest commit: 5c9bcde

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

Warning

Rate limit exceeded

@mcansh has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 32 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 15 minutes and 32 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 76f76de4-843d-4b07-b872-17a47f9a8047

📥 Commits

Reviewing files that changed from the base of the PR and between dbe5879 and 5c9bcde.

📒 Files selected for processing (1)
  • app/router.ts

Walkthrough

Refactors authentication from a session/async-context approach to remix/auth credentials-based auth, adds credential provider and session parsing helpers, updates middleware wiring to use function references, changes controller action signatures to accept a context object, and adjusts redirects and session shape for auth state.

Changes

Cohort / File(s) Summary
Auth Middleware & Router
app/middleware/auth.ts, app/router.ts
Replaced previous loadAuth/requireAuth implementations with remix/auth-based scheme (createSessionAuthScheme), added password credentials provider, safe return-to helpers, and appended loadAuth() into global router middleware chain.
Auth Utilities
app/utils/auth-session.ts, app/utils/local-schema.ts
Added auth-session helpers: AuthSession, AuthIdentity, normalizeEmail(), parseAuthSession(); added // @ts-expect-error`` annotations in local-schema.ts.
Removed Context Utilities
app/utils/context.ts
Removed request-scoped async-context helpers (getCurrentUser, getCurrentUserSafely, setCurrentUser).
Controllers
app/auth/controller.tsx, app/home/controller.tsx, app/history/controller.tsx
Changed action signatures to accept context, switch to reading auth via context.get(Auth), replaced middleware invocations from [requireAuth()] to [requireAuth], updated login/register/logout flows to use verify/completeAuth, updated session "auth" shape and return-to/redirect handling.
User Model
app/models/user.ts
Removed exported loginSchema, LoginData, JoinData type, and getUserById(); kept other user functions unchanged.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant AuthCtl as Auth Controller
    participant PasswordProv as Password Provider
    participant DB as Database
    participant Session as SessionStore
    participant Router as Router

    Client->>AuthCtl: POST /login (form data)
    AuthCtl->>PasswordProv: verifyCredentials(passwordProvider, context)
    PasswordProv->>DB: find user by email
    DB-->>PasswordProv: user record
    PasswordProv->>PasswordProv: bcrypt.compare(password, hash)
    alt credentials valid
        PasswordProv-->>AuthCtl: identity
        AuthCtl->>Session: completeAuth(context) / session.set("auth",{userId})
        AuthCtl->>Router: getPostAuthRedirect(context.url)
        AuthCtl-->>Client: Redirect to target
    else invalid / error
        AuthCtl->>Session: flash error
        AuthCtl-->>Client: Redirect to /login with returnTo
    end

    Client->>Router: GET /protected
    Router->>Session: read "auth" session value
    Session-->>Router: AuthSession { userId }
    Router->>DB: load user by id
    DB-->>Router: user (no password)
    Router-->>ProtectedHandler: attach AuthIdentity to request
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

  • feat: migrate to remix v3 #98: Related migration to new Remix fetch-router/auth patterns — similar changes to auth middleware, controller signatures, and session handling.
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.27% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title 'feat: migrate to remix-run/auth-middleware' directly and concisely describes the main change: migrating to a new authentication middleware library.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch logan/auth-middleware

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR migrates authentication to remix/auth-middleware, updates security header behavior by request method, removes legacy user-context/session helpers, and extracts a reusable keyboard UI that’s now shown on historical games.

Changes:

  • Migrate auth to remix/auth-middleware with session-based scheme + credentials provider, and update controllers to read Auth identity.
  • Add skip() support usage for security headers based on request method.
  • Extract the on-screen keyboard into app/components/keyboard.tsx and reuse it on home + historical game pages.

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
app/utils/local-schema.ts Adds @ts-expect-error suppressions around internal ~run calls while iterating keys/items/variants.
app/utils/local-form-schema.ts Introduces a local Standard Schema-compatible form data parser that plugs into local-schema.
app/utils/context.ts Removes async-context-based current-user storage utilities.
app/utils/auth-session.ts Adds auth session normalization/parsing helpers (e.g., parseAuthSession, normalizeEmail).
app/router.ts Adds loadAuth() middleware globally and configures security headers skipping by method.
app/models/user.ts Removes unused login schema/types and getUserById.
app/middleware/auth.ts Replaces old middleware with auth() scheme + credentials provider + requireAuth.
app/home/page.tsx Reuses extracted Keyboard component; updates tab behavior for read-only inputs.
app/home/controller.tsx Switches auth access from context-user to Auth identity; updates middleware usage.
app/history/game.tsx Shows the keyboard on historical game pages (replacing the prior submit UI).
app/history/controller.tsx Switches auth access to Auth identity; updates middleware usage.
app/components/keyboard.tsx New extracted keyboard UI component.
app/auth/controller.tsx Updates login flow to use credentials provider + auth middleware session handling.
Comments suppressed due to low confidence (4)

app/models/user.ts:18

  • getUserById was removed from this module, but it is still referenced in app/home/controller.test.ts (used in a vi.mock return object). This will break type-checking/build of tests. Update the test to mock the new auth/session-based flow (or remove the unused mock) so it no longer expects getUserById to exist.


export async function getUserByEmail(email: User["email"]) {
	return db.user.findUnique({ where: { email } })
}

app/utils/local-schema.ts:701

  • Avoid using a bare // @ts-expect-error here. It suppresses the real typing issue (indexing shape with a string) and can hide unrelated future errors. Prefer typing the keys (e.g. iterate Object.keys(shape) as Array<keyof shape>) or otherwise narrow key so shape[key] is a Schema, and include a short justification if suppression is still required.
    app/utils/local-schema.ts:975
  • Avoid using a bare // @ts-expect-error here. The underlying issue is that items[index] can’t be typed safely when index is a number. Consider iterating with a typed index (for (const [index, schema] of items.entries())) or otherwise narrowing/casting so the call to schema["~run"] doesn’t require suppressing errors, and include a justification if suppression remains necessary.
    app/utils/local-schema.ts:1072
  • Avoid using a bare // @ts-expect-error here. The cast tag as keyof variants is already narrowing, so it’s better to refine the generic constraints / discriminant typing so variants[tag] is known to be a Schema and schema["~run"] can be called without suppressing type errors. If suppression is required, add a short explanation so future changes don’t accidentally rely on it.

Comment thread app/auth/controller.tsx Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/router.ts (1)

42-45: ⚠️ Potential issue | 🟠 Major

Don't drop security headers on every non-GET response.

forgotPassword.action and resetPassword.action still render HTML on POST, so this strips CSP, framing, and HSTS protections from real document responses. If the goal is to exempt API/file responses, decide that from the response type rather than the request method.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/router.ts` around lines 42 - 45, The current skip callback on
securityHeaders drops security headers for all non-GET requests, which removes
CSP/HSTS for POST handlers that still return HTML (e.g., forgotPassword.action
and resetPassword.action); change the skip(context) logic to decide based on
response type rather than request method — e.g., only return true (skip) for
API/file responses by checking the response Content-Type or the request Accept
header (or Content-Disposition) and ensure document/html responses (or routes
known to render HTML like forgotPassword.action/resetPassword.action) keep
security headers.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/auth/controller.tsx`:
- Around line 193-194: The flashed error isn't consumed because
register.actions.index never reads the Session; update register.actions.index to
read the Session (e.g., via the same Session/getSession utility used elsewhere),
extract the "error" flash key and pass it into the register page's loader/props
so the UI can render it, or alternatively stop redirecting from the controller
and return the register page response directly with an inline error prop;
specifically adjust the controller code that calls session.flash(...) and
routes.auth.register.index.href and the register.actions.index loader/renderer
to exchange the Session flash "error" so duplicate-email submissions show the
message.
- Around line 197-199: The session is being set to a bare string
(session.set("auth", user.id)) but parseAuthSession() expects an object shape {
userId: string }, so update the session assignment after createUser(result) to
store the correct shape (an object containing userId) so subsequent requests are
recognized as authenticated; locate the session.set call in the auth controller
and replace the bare string with the object form { userId: user.id } (or the
exact key name parseAuthSession expects) to persist the proper auth session
shape.

In `@app/history/controller.tsx`:
- Around line 23-25: The auth fallback in the history controller currently
redirects unauthenticated requests with redirect(routes.auth.login.index.href())
which loses the original URL; update the branch that checks auth (where you call
context.get(Auth) and test auth.ok === false) to include the return-to query by
using getReturnToQuery(context.url) when building the login redirect (i.e.,
append the helper's output to routes.auth.login.index.href()); also mirror this
pattern for any other auth fallbacks in this controller so the requested history
URL is preserved post-login.
- Around line 20-25: The index action currently checks auth.ok and redirects to
login; update index to pass the original requested URL as a returnTo param when
calling redirect(routes.auth.login.index.href()) so the user returns after
sign-in, and add the same defensive auth validation to the game action: retrieve
auth from context.get(Auth) and guard on (auth.ok === false) returning the same
redirect with returnTo; reference the Auth type and GoodAuth/BadAuth union and
the game and index action handlers to implement the checks consistently.

In `@app/middleware/auth.ts`:
- Around line 29-40: The verify handler in createSessionAuthScheme is returning
the full Prisma User row (from db.user.findFirst) so protected handlers receive
sensitive fields like password; change the query in verify to explicitly select
only the public fields needed for authentication (e.g., id, email, name,
createdAt) instead of the entire record, build and return a redacted identity
object (e.g., { user: { id, email, name } }) and update the AuthIdentity
type/signature to match that sanitized shape; keep parseAuthSession and
AuthSession usage intact but ensure verify returns the narrowed AuthIdentity
rather than the full User.

---

Outside diff comments:
In `@app/router.ts`:
- Around line 42-45: The current skip callback on securityHeaders drops security
headers for all non-GET requests, which removes CSP/HSTS for POST handlers that
still return HTML (e.g., forgotPassword.action and resetPassword.action); change
the skip(context) logic to decide based on response type rather than request
method — e.g., only return true (skip) for API/file responses by checking the
response Content-Type or the request Accept header (or Content-Disposition) and
ensure document/html responses (or routes known to render HTML like
forgotPassword.action/resetPassword.action) keep security headers.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1215b16a-da55-49de-973d-5ae9eab49435

📥 Commits

Reviewing files that changed from the base of the PR and between f4409ed and 604ea4b.

📒 Files selected for processing (10)
  • app/auth/controller.tsx
  • app/history/controller.tsx
  • app/home/controller.tsx
  • app/middleware/auth.ts
  • app/models/user.ts
  • app/router.ts
  • app/utils/auth-session.ts
  • app/utils/context.ts
  • app/utils/local-form-schema.ts
  • app/utils/local-schema.ts
💤 Files with no reviewable changes (1)
  • app/utils/context.ts

Comment thread app/auth/controller.tsx
Comment thread app/auth/controller.tsx Outdated
Comment thread app/history/controller.tsx Outdated
Comment thread app/history/controller.tsx Outdated
Comment thread app/middleware/auth.ts
mcansh added 2 commits April 4, 2026 00:44
Signed-off-by: Logan McAnsh <logan@mcan.sh>
Signed-off-by: Logan McAnsh <logan@mcan.sh>
@mcansh mcansh force-pushed the logan/auth-middleware branch from fd203d5 to 22a9abc Compare April 4, 2026 04:46
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

Note

Unit test generation is a public access feature. Expect some limitations and changes as we gather feedback and continue to improve it.


Generating unit tests... This may take up to 20 minutes.

@mcansh mcansh merged commit 31108a9 into main Apr 4, 2026
5 checks passed
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 10 changed files in this pull request and generated 5 comments.

Comments suppressed due to low confidence (3)

app/utils/local-schema.ts:700

  • New // @ts-expect-error is masking a typing issue caused by indexing shape with a string key. Prefer narrowing key to keyof shape (e.g., a typed Object.keys cast or for...in) so the call to ~run type-checks without suppressing errors; this keeps future refactors from accidentally hiding real schema type problems.
    app/utils/local-schema.ts:973
  • New // @ts-expect-error is suppressing a type-safety issue when indexing items/value by index. It would be better to adjust the loop typing (or cast items[index] to Schema) so the ~run call is checked by TypeScript rather than silenced.
    app/utils/local-schema.ts:1072
  • New // @ts-expect-error is hiding a typing mismatch around variants[tag] and schema["~run"]. Consider tightening the discriminator/key typing so schema is known to be a Schema and the ~run call can be made without suppressing type errors.

Comment thread app/models/user.ts
Comment on lines 9 to 17
export const joinSchema = f.object({
email: f.field(s.string().pipe(email())),
username: f.field(s.string().pipe(minLength(1))),
password: f.field(s.string().pipe(minLength(10))),
})

export type JoinData = s.InferOutput<typeof joinSchema>
export type LoginData = s.InferOutput<typeof loginSchema>

export const loginSchema = f.object({
email: f.field(s.string().pipe(email())),
password: f.field(s.string().pipe(minLength(10))),
})

export async function getUserById(id: User["id"]) {
return db.user.findUnique({ where: { id } })
}

export async function getUserByEmail(email: User["email"]) {
return db.user.findUnique({ where: { email } })
}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

getUserById was removed, but there are still tests referencing it (e.g. app/home/controller.test.ts mocks getUserById). Please update or remove those usages/mocks to avoid TypeScript/test failures.

Copilot uses AI. Check for mistakes.
Comment thread app/middleware/auth.ts
setCurrentUser(user)
if (typeof user.password === "string" && user.password !== "") {
let verified = await bcrypt.compare(input.password, user.password)
return verified ? user : null
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

passwordProvider.verify returns the full User record (including the hashed password) on success. Even if it’s not currently exposed, returning password-bearing objects increases the chance of accidental leakage; consider returning a user object with password omitted/stripped after verification (while still fetching it for bcrypt comparison).

Suggested change
return verified ? user : null
if (verified === false) {
return null
}
let { password: _password, ...safeUser } = user
return safeUser

Copilot uses AI. Check for mistakes.
Comment thread app/auth/controller.tsx
let error = session.get("error")

return render(
<Document url={url} head={<title>Login - Remix Wordle</title>}>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The register page title is still set to "Login - Remix Wordle". Since this hunk was updated, it’s a good opportunity to correct the title to match the register page to avoid confusing users and improve accessibility (page title is announced by screen readers).

Suggested change
<Document url={url} head={<title>Login - Remix Wordle</title>}>
<Document url={url} head={<title>Register - Remix Wordle</title>}>

Copilot uses AI. Check for mistakes.
Comment thread app/auth/controller.tsx
<main class="h-dvh">
<form
method="POST"
action={routes.auth.register.action.href()}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The register form action does not preserve the returnTo query param (unlike the login form). If a user is sent to register from a protected page, the post-register redirect can’t send them back; consider including getReturnToQuery(url) when building the register form action URL.

Suggested change
action={routes.auth.register.action.href()}
action={routes.auth.register.action.href(undefined, getReturnToQuery(url))}

Copilot uses AI. Check for mistakes.
Comment thread app/auth/controller.tsx
Comment on lines +203 to 207
let user = await createUser(result)

session.set("auth", {auth: user.id})
session.set("auth", { userId: user.id })

return redirect(routes.home.index.href())
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

After successful registration, the code sets session.set("auth", ...) but does not call completeAuth(context) (used in the login flow) to finalize auth/session state (e.g., session id regeneration to prevent session fixation). Consider using the same completeAuth + getPostAuthRedirect pattern here for consistency and security.

Copilot uses AI. Check for mistakes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 4, 2026

✅ Created PR with unit tests: #134

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants