Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
101 changes: 44 additions & 57 deletions app/auth/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,43 @@
import { completeAuth, verifyCredentials } from "remix/auth"
import { parse } from "remix/data-schema"
import type { Controller } from "remix/fetch-router"
import { createRedirectResponse as redirect } from "remix/response/redirect"
import { redirect } from "remix/response/redirect"
import { Session } from "remix/session"

import { Document } from "../components/document.tsx"
import { loadAuth } from "../middleware/auth.ts"
import { getPostAuthRedirect, getReturnToQuery, passwordProvider } from "../middleware/auth.ts"
import {
authenticateUser,
createPasswordResetToken,
createUser,
getUserByEmail,
joinSchema,
loginSchema,
resetPassword,
} from "../models/user.ts"
import { routes } from "../routes.ts"
import { render } from "../utils/render.ts"

export const auth = {
middleware: [loadAuth()],
middleware: [],
actions: {
login: {
actions: {
index({ get, url }) {
let session = get(Session)
let error = session.get("error")
let formAction = routes.auth.login.action.href(undefined, {
returnTo: url.searchParams.get("returnTo"),
})
let formAction = routes.auth.login.action.href(undefined, getReturnToQuery(url))

return render(
<Document url={url} head={<title>Login - Remix Wordle</title>}>
<main class="h-dvh">
{error && typeof error === "string" ? (
<div class="text-red-500">{error}</div>
) : null}
<form
method="post"
class="mx-auto flex h-full w-full max-w-md flex-col items-center justify-center space-y-6 px-8"
action={formAction}
>
>
<div class="w-full">
{error && typeof error === "string" ? (
<div class="text-red-500 mb-1">{error}</div>
) : null}
<label htmlFor="email" class="block text-sm font-medium text-gray-700">
Email address
</label>
Expand Down Expand Up @@ -90,36 +87,47 @@ export const auth = {
)
},

async action({ get, url }) {
let session = get(Session)
let formData = get(FormData)
let result = parse(loginSchema, formData)
let returnTo = url.searchParams.get("returnTo")

let user = await authenticateUser(result.email, result.password)
if (!user) {
session.flash("error", "Invalid email or password. Please try again.")
return redirect(routes.auth.login.index.href(undefined, { returnTo }))
async action(context) {
try {
let user = await verifyCredentials(passwordProvider, context)

if (user == null) {
let session = context.get(Session)
session.flash("error", "Invalid email or password. Please try again.")
return redirect(
routes.auth.login.index.href(undefined, getReturnToQuery(context.url)),
)
}

let session = completeAuth(context)
session.set("auth", { userId: user.id })
return redirect(getPostAuthRedirect(context.url))
} catch (error) {
let session = context.get(Session)
session.flash("error", "We could not complete that sign-in request.")
return redirect(routes.auth.login.index.href(undefined, getReturnToQuery(context.url)))
}

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

return redirect(returnTo ?? routes.home.index.href())
},
},
},

register: {
actions: {
index({ url }) {
index({ get, url }) {
let session = get(Session)
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.
<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.
class="mx-auto flex h-full w-full max-w-md flex-col items-center justify-center space-y-6 px-8"
>
>
{error && typeof error === "string" ? (
<div class="text-red-500 mb-1">{error}</div>
) : null}
<div class="w-full">
<label class="block text-sm font-medium text-gray-700" for="email">
Email address
Expand Down Expand Up @@ -188,44 +196,23 @@ export const auth = {

// Check if user already exists
if (await getUserByEmail(result.email)) {
return render(
<Document url={url} head={<title>Login - Remix Wordle</title>}>
<div class="card" style="max-width: 500px; margin: 2rem auto;">
<div class="alert alert-error">An account with this email already exists.</div>
<p>
<a href={routes.auth.register.index.href()} class="btn">
Back to Register
</a>
<a
href={routes.auth.login.index.href()}
class="btn btn-secondary"
style="margin-left: 0.5rem;"
>
Login
</a>
</p>
</div>
</Document>,
{ status: 400 },
)
session.flash("error", "An account with this email already exists.")
return redirect(routes.auth.register.index.href(undefined, getReturnToQuery(url)))
Comment thread
mcansh marked this conversation as resolved.
}

let user = await createUser({
email: result.email,
username: result.username,
password: result.password,
})
let user = await createUser(result)

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

return redirect(routes.home.index.href())
Comment on lines +203 to 207
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.
},
},
},

logout({ get }) {
let session = get(Session)
session.destroy()
logout(context) {
let session = context.get(Session)
session.unset("auth")
session.regenerateId(true)
return redirect(routes.home.index.href())
},

Expand Down
19 changes: 12 additions & 7 deletions app/history/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Auth, type BadAuth, type GoodAuth } from "remix/auth-middleware"
import type { Controller } from "remix/fetch-router"
import { redirect } from "remix/response/redirect"

import { requireAuth } from "../middleware/auth.ts"
import { getReturnToQuery, requireAuth } from "../middleware/auth.ts"
import { getGameById, isGameComplete } from "../models/game.ts"
import { routes } from "../routes.ts"
import { getCurrentUser } from "../utils/context.ts"
import type { AuthIdentity } from "../utils/auth-session.ts"
import { db } from "../utils/db.ts"
import { render } from "../utils/render.ts"
import { HistoricalGame } from "./game.tsx"
Expand All @@ -15,13 +17,16 @@ import {
import { GameNotFound } from "./not-found-page.tsx"

export let history = {
middleware: [requireAuth()],
middleware: [requireAuth],
actions: {
async index({ url }) {
let user = getCurrentUser()
async index(context) {
let auth = context.get(Auth) as GoodAuth<AuthIdentity> | BadAuth
if (auth.ok === false) {
return redirect(routes.auth.login.index.href(undefined, getReturnToQuery(context.url)))
}

let games = await db.game.findMany({
where: { userId: user.id },
where: { userId: auth.identity.user.id },
orderBy: { createdAt: "desc" },
select: HISTORICAL_GAME_SELECT,
})
Expand All @@ -30,7 +35,7 @@ export let history = {
return createHistoricalGameListItem(game)
})

return render(<HistoricalGameList setup={{ url }} games={formattedGames} />)
return render(<HistoricalGameList setup={{ url: context.url }} games={formattedGames} />)
},

async game({ params, url }) {
Expand Down
43 changes: 26 additions & 17 deletions app/home/controller.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import { Auth, type BadAuth, type GoodAuth } from "remix/auth-middleware"
import type { Controller } from "remix/fetch-router"
import { createRedirectResponse } from "remix/response/redirect"
import { createRedirectResponse, redirect } from "remix/response/redirect"
import { Session } from "remix/session"

import { REVEAL_WORD, WORD_LENGTH } from "../constants.ts"
import { requireAuth } from "../middleware/auth.ts"
import { getReturnToQuery, requireAuth } from "../middleware/auth.ts"
import { createGuess, getFullBoard, getTodaysGame, isGameComplete } from "../models/game.ts"
import { routes } from "../routes.ts"
import { getCurrentUser } from "../utils/context.ts"
import type { AuthIdentity } from "../utils/auth-session.ts"
import * as f from "../utils/local-form-schema.ts"
import * as s from "../utils/local-schema.ts"
import { render } from "../utils/render.ts"
import * as f from "./local-form-schema.ts"
import * as s from "./local-schema.ts"
import { Page } from "./page.tsx"

export function validLength(length: number): s.Check<Array<string>> {
Expand Down Expand Up @@ -37,12 +38,16 @@ export const guessWordSchema = f.object({
})

export const home = {
middleware: [requireAuth()],
middleware: [requireAuth],
actions: {
async action({ get }) {
let session = get(Session)
let formData = get(FormData)
let user = getCurrentUser()
async action(context) {
let auth = context.get(Auth) as GoodAuth<AuthIdentity> | BadAuth
if (auth.ok === false) {
return redirect(routes.auth.login.index.href(undefined, getReturnToQuery(context.url)))
}

let session = context.get(Session)
let formData = context.get(FormData)

let data = s.parseSafe(guessWordSchema, formData)

Expand All @@ -52,7 +57,7 @@ export const home = {
}

let guessedWord = data.value.letters.join("")
let error = await createGuess(user.id, guessedWord)
let error = await createGuess(auth.identity.user.id, guessedWord)

if (error) {
console.error({ error })
Expand All @@ -64,16 +69,20 @@ export const home = {
)
},

async index({ get, url }) {
let session = get(Session)
let user = getCurrentUser()
async index(context) {
let auth = context.get(Auth) as GoodAuth<AuthIdentity> | BadAuth
if (auth.ok === false) {
return redirect(routes.auth.login.index.href(undefined, getReturnToQuery(context.url)))
}

let session = context.get(Session)

let game = await getTodaysGame(user.id)
let game = await getTodaysGame(auth.identity.user.id)
let board = getFullBoard(game)

let showModal = isGameComplete(game.status)

let showWord = showModal || url.searchParams.has(REVEAL_WORD) ? board.word : undefined
let showWord = showModal || context.url.searchParams.has(REVEAL_WORD) ? board.word : undefined

let errorMessage = session.get("error") || undefined

Expand All @@ -83,7 +92,7 @@ export const home = {

return render(
<Page
setup={{ url }}
setup={{ url: context.url }}
showModal={showModal}
showWord={showWord}
board={board}
Expand Down
Loading
Loading