Skip to content

Add password reset flow#234

Merged
auerbachb merged 9 commits into
mainfrom
cursor/password-reset-ad4b
Apr 26, 2026
Merged

Add password reset flow#234
auerbachb merged 9 commits into
mainfrom
cursor/password-reset-ad4b

Conversation

@auerbachb
Copy link
Copy Markdown
Owner

@auerbachb auerbachb commented Apr 25, 2026

Summary

  • Add DB-backed single-use password reset tokens with expiring signed links.
  • Add request and confirm API routes, reset email delivery, and reset web UI.
  • Add discoverable web/iOS forgot-password entry points plus env and abuse notes.
  • Rebase onto latest main and address all visible Bugbot and CodeRabbit comments, including latest follow-up feedback.

Review comments addressed

  • Made the manual SQL migration idempotent for the FK constraint and active-token index.
  • Aligned reset email expiry copy with the 60-minute token TTL.
  • Kept reset request response shape/status identical for unknown, repeated, rate-limited, and provider-failure paths.
  • Serialized per-user reset-token issuance with a transaction/advisory lock and active-token uniqueness.
  • Made password reset confirmation handle malformed JSON as 400 and consume the token in the same transaction as password update.
  • Logged reset links in non-production when email provider env vars are unset, while failing fast for missing production email config.
  • Added a 10s timeout and defensive HTML escaping to the email provider path.
  • Removed email PII from reset page URLs and reset JWT payloads.
  • Added semantic form submission and accessible labels to the reset form; clear email after successful reset request.
  • Aligned E2E mocks with validation vs expired-token response contracts and stabilized WebKit form interactions.
  • Documented and bounded the process-local rate limiter.
  • Cleaned up the iOS in-flight reset guard.

Walkthrough

password_reset_web_flow.mp4
Reset request page with prefilled email
Set new password page

Testing

  • npm run build passes.
  • npx tsc --noEmit passes.
  • PORT=3012 npx playwright test e2e/auth/password-reset.spec.ts passes across mobile Chromium and WebKit iPhone projects.
  • npm run test:unit currently fails in pre-existing rebased src/lib/thoughtSaving.test.ts because it uses node:test under the new Vitest runner; unrelated to this password reset PR.

Notes

  • No secrets are committed; .env.example documents placeholders only.
  • swift test could not run because Swift is not installed on this Linux image; GitHub iOS workflows will rerun on push.

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Summary by CodeRabbit

  • New Features

    • Password reset via email across web and iOS with single-use links (1‑hour expiry), generic success messaging, per-email+IP rate limits, Reset Password page, and a “Forgot password?” entry; UI shows in-progress state and disables actions during requests.
  • Tests

    • New end-to-end and iOS UI tests covering request and confirm flows.
  • Documentation

    • README and .env.example updated with optional email settings, public app URL default, and dev behavior (reset links logged locally when email delivery isn’t configured).

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
still-point Ready Ready Preview, Comment Apr 25, 2026 4:40pm

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

📝 Walkthrough

Walkthrough

Adds a complete password-reset feature: DB table + migration, server token creation/verification and rate-limiting, Resend-backed email delivery with dev fallback, request/confirm API routes, web reset page and Auth UI link, iOS UI/API changes, tests, and fixture updates.

Changes

Cohort / File(s) Summary
Database / Schema / Migration
drizzle/password_reset_tokens_incremental.sql, src/db/schema.ts
Add password_reset_tokens table, relations, indexes, and migration to ensure constraints and a partial index for active tokens.
Server: APIs & Utilities
src/app/api/auth/password-reset/request/route.ts, src/app/api/auth/password-reset/confirm/route.ts, src/lib/passwordReset.ts, src/lib/email.ts, src/middleware.ts
New POST endpoints for request/confirm, token generation/verification/consumption, in-memory per-process rate limiting, token hashing, email sending (Resend with dev log fallback), and middleware allowlist addition.
Web UI
src/app/reset-password/page.tsx, src/components/AuthScreen.tsx
Add reset-password page handling email & token flows, client validation/submission states, and a "Forgot password?" link in login mode.
Mobile (iOS)
ios/StillPointApp/ViewModels/AuthViewModel.swift, ios/StillPointApp/Views/AuthView.swift, ios/StillPointShared/Sources/StillPointShared/APIClient.swift, ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
iOS ViewModel/UI state and API client/DTO additions to request password reset and show success/error UI.
Tests / Fixtures
e2e/auth/password-reset.spec.ts, e2e/fixtures/auth.fixture.ts, ios/StillPointAppUITests/StillPointAppUITests.swift
Playwright E2E tests for reset flow, mock fixture endpoints/state for request/confirm, and an iOS UI test verifying discoverability of the reset entry.
Env / Docs
.env.example, README.md
Document NEW env placeholders (NEXT_PUBLIC_APP_URL, EMAIL_FROM, RESEND_API_KEY), reset semantics, rate limits, and Resend usage; .env.example adds commented config entries.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant Web as Web App
  participant API as PasswordReset API
  participant DB as Database
  participant Mail as Resend
  participant IOS as iOS App

  User->>Web: Visit /reset-password (no token) submit email
  Web->>API: POST /api/auth/password-reset/request { email, ipHash }
  API->>DB: Lookup user by email
  alt user exists
    API->>DB: TX: lock user, find/create password_reset_tokens row
    DB-->>API: tokenHash (server retains plaintext token to email)
    API->>Mail: send password reset email with token link
    Mail-->>API: delivered:true/false
  else user missing
    DB-->>API: no user
  end
  API-->>Web: { message: generic success }
  Web-->>User: show generic success message

  User->>Web: Visit /reset-password?token=... submit new password
  Web->>API: POST /api/auth/password-reset/confirm { token, password }
  API->>DB: TX: consume token row (usedAt null & not expired) and update users.passwordHash
  DB-->>API: rowsUpdated (1) / 0
  alt rowsUpdated == 1
    API-->>Web: { ok: true }
  else
    API-->>Web: 400 invalid/expired token
  end
  Web-->>User: show success or error
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

"I hopped through code with nimble paws,
A token tucked in secret claws.
I send a link across the glen,
Reset your keys and hop again.
🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title "Add password reset flow" directly and clearly summarizes the primary change across the entire changeset, covering the main objective of implementing password reset functionality.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cursor/password-reset-ad4b

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

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 6 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit f46cb87. Configure here.

Comment thread src/lib/email.ts Outdated
Comment thread src/app/api/auth/password-reset/request/route.ts Outdated
Comment thread ios/StillPointApp/ViewModels/AuthViewModel.swift
Comment thread src/lib/email.ts
Comment thread src/lib/email.ts
Comment thread src/app/api/auth/password-reset/confirm/route.ts Outdated
Copy link
Copy Markdown

@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: 10

Caution

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

⚠️ Outside diff range comments (2)
src/middleware.ts (1)

10-24: ⚠️ Potential issue | 🟠 Major

Over-broad prefix allowlist can unintentionally bypass auth.

Line 23 uses startsWith, so adding "/api/auth/password-reset" at Line 10 also whitelists any path beginning with that prefix (not just /request and /confirm). Prefer explicit endpoint allowlisting here.

Safer route matching
 const publicPaths = [
   "/api/auth/signup",
   "/api/auth/login",
   "/api/auth/logout",
-  "/api/auth/password-reset",
   "/api/board",
 ];
+const publicExactPaths = new Set([
+  "/api/auth/password-reset/request",
+  "/api/auth/password-reset/confirm",
+]);

 // Allow public routes
-if (publicPaths.some((p) => pathname.startsWith(p))) {
+if (publicExactPaths.has(pathname) || publicPaths.some((p) => pathname.startsWith(p))) {
   return NextResponse.next();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/middleware.ts` around lines 10 - 24, The allowlist in publicPaths plus
the middleware check in middleware(...) uses pathname.startsWith(p) which
over-broadly matches prefixes (e.g., "/api/auth/password-reset" will match
"/api/auth/password-reset-extra"); change the matching to exact-path checks (or
a Set/includes) or use anchored regexes so only exact endpoints are allowed:
update the publicPaths handling in middleware to compare pathname === p (or test
^p$) for each entry in publicPaths instead of startsWith, and ensure publicPaths
entries list the specific endpoints (e.g., "/api/auth/password-reset/request"
and "/api/auth/password-reset/confirm") so only those exact routes bypass auth.
ios/StillPointApp/ViewModels/AuthViewModel.swift (1)

5-75: ⚠️ Potential issue | 🟠 Major

Add @MainActor to AuthViewModel and use the API-returned message.

The class mutates observable properties (email, isSubmitting, error, resetMessage, isRequestingPasswordReset) across async boundaries in submit() and requestPasswordReset(). Mark the class as @MainActor to ensure explicit main-thread isolation for these mutations:

`@Observable`
`@MainActor`
final class AuthViewModel {
  // ...
}

Additionally, requestPasswordReset() should use the message returned by the API instead of hardcoding a response. The APIClient.requestPasswordReset(email:) method already returns a String, so assign it to resetMessage:

try await APIClient.shared.requestPasswordReset(email: trimmedEmail)
resetMessage = try await APIClient.shared.requestPasswordReset(email: trimmedEmail)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/ViewModels/AuthViewModel.swift` around lines 5 - 75, Mark
AuthViewModel as `@MainActor` to ensure main-thread isolation for properties
mutated across async boundaries (email, isSubmitting, error, resetMessage,
isRequestingPasswordReset), e.g. add `@MainActor` above the final class
AuthViewModel declaration; and in requestPasswordReset() capture and assign the
API-returned message to resetMessage by calling
APIClient.shared.requestPasswordReset(email: trimmedEmail) once and storing its
returned String instead of using the hardcoded message (refer to the
requestPasswordReset() method and APIClient.shared.requestPasswordReset).
🧹 Nitpick comments (5)
.env.example (1)

18-21: EMAIL_PROVIDER is documented but not used by the runtime.

Line 19 implies provider selection, but current server code (src/lib/email.ts) always targets Resend and only gates on EMAIL_FROM + RESEND_API_KEY. Consider either implementing provider switching or removing/clarifying EMAIL_PROVIDER to avoid misleading configuration.

Suggested doc-only alignment
-# Optional email provider config for password reset delivery. If unset, reset links are logged server-side in non-production only.
-# EMAIL_PROVIDER=console
+# Optional email config for password reset delivery (Resend currently implemented).
+# If EMAIL_FROM or RESEND_API_KEY is unset, reset links are logged server-side in non-production only.
+# EMAIL_PROVIDER=resend
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 18 - 21, The .env.example documents EMAIL_PROVIDER
but the runtime in src/lib/email.ts ignores it and always uses Resend (gated
only by EMAIL_FROM and RESEND_API_KEY); either remove/clarify EMAIL_PROVIDER
from the example or make src/lib/email.ts honor it. Fix by updating
src/lib/email.ts to read process.env.EMAIL_PROVIDER and branch the sending logic
(e.g., support 'resend' and 'console' modes), wiring the existing Resend client
initialization (RESEND_API_KEY, EMAIL_FROM) when EMAIL_PROVIDER === 'resend' and
falling back to the current console/log behavior when EMAIL_PROVIDER ===
'console' (update the sendEmail/sendPasswordResetEmail functions to consult
EMAIL_PROVIDER). Ensure env vars EMAIL_FROM and RESEND_API_KEY remain used for
Resend mode and update .env.example accordingly if you choose the doc-only path.
ios/StillPointApp/Views/AuthView.swift (1)

101-121: Consider scoping reset confirmation text to login mode only.

Right now the message at Lines 101-107 can persist even after switching to sign-up. Limiting it to login mode avoids cross-flow UI noise.

Small conditional tweak
-                    if let resetMessage = vm.resetMessage {
+                    if !vm.isSignUp, let resetMessage = vm.resetMessage {
                         Text(resetMessage)
                             .font(SPFont.mono(12))
                             .foregroundStyle(SPColor.greenText)
                             .multilineTextAlignment(.center)
                             .accessibilityIdentifier("auth.passwordResetMessage")
                     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/Views/AuthView.swift` around lines 101 - 121, The reset
confirmation Text currently shown by checking vm.resetMessage can persist when
the view switches to sign-up; restrict it to login mode by rendering the Text
only when vm.resetMessage is non-nil AND vm.isSignUp is false (or move the
entire vm.resetMessage Text block inside the existing if !vm.isSignUp block).
Update the view so the UI uses the combined condition (e.g., vm.resetMessage !=
nil && !vm.isSignUp) to avoid showing resetMessage during sign-up flows and keep
the Button and its disabled/label behavior unchanged.
src/db/schema.ts (1)

34-42: Avoid duplicate uniqueness definitions on tokenHash.

Line 34 already marks tokenHash unique, and Line 41 adds another unique index on the same column. Keep one to avoid redundant constraints/index overhead.

Suggested fix
-  tokenHash: varchar("token_hash", { length: 64 }).unique().notNull(),
+  tokenHash: varchar("token_hash", { length: 64 }).notNull(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema.ts` around lines 34 - 42, The tokenHash column is declared
unique via tokenHash: varchar("token_hash", { length: 64 }).unique().notNull(),
and you also create a
uniqueIndex("password_reset_tokens_token_hash_unique").on(table.tokenHash) —
remove the redundant unique index definition (the uniqueIndex call) so there is
only one uniqueness constraint on tokenHash; keep the column-level .unique() on
tokenHash (or if you prefer index-style uniqueness, remove .unique() and keep
the uniqueIndex) and update the table indexes block to omit the duplicate
uniqueIndex reference.
ios/StillPointApp/ViewModels/AuthViewModel.swift (1)

68-70: Use the API’s returned reset message instead of hardcoding.

Line 68-70 discards APIClient.shared.requestPasswordReset(email:)’s returned message and duplicates the copy locally. Prefer using the returned string as the single source of truth.

Suggested fix
-            try await APIClient.shared.requestPasswordReset(email: trimmedEmail)
-            resetMessage = "If an account exists for that email, a reset link will arrive shortly."
+            resetMessage = try await APIClient.shared.requestPasswordReset(email: trimmedEmail)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@ios/StillPointApp/ViewModels/AuthViewModel.swift` around lines 68 - 70, The
reset handler in AuthViewModel currently ignores the string returned by
APIClient.shared.requestPasswordReset(email:) and instead assigns a hardcoded
message to resetMessage; change it to capture and assign the API's returned
string (e.g., let message = try await
APIClient.shared.requestPasswordReset(email: trimmedEmail); resetMessage =
message) so resetMessage uses the API response as the single source of truth and
remove the duplicate hardcoded text.
src/lib/passwordReset.ts (1)

118-144: Consider periodic cleanup of expired rate-limit entries.

The in-memory Map grows unbounded since expired entries are never removed—they're only replaced when the same key is reused. For long-running processes with diverse email/IP combinations, this could accumulate stale entries.

Given the PR notes recommend WAF throttling for production, this is acceptable as a lightweight safeguard, but adding periodic cleanup would improve memory hygiene.

♻️ Optional: Add lazy cleanup of expired entries
 function attemptsStore() {
   if (!globalForPasswordReset.__passwordResetAttempts) {
     globalForPasswordReset.__passwordResetAttempts = new Map();
   }
   return globalForPasswordReset.__passwordResetAttempts;
 }

+function cleanupExpiredAttempts() {
+  const now = Date.now();
+  const store = attemptsStore();
+  for (const [key, attempt] of store) {
+    if (attempt.resetAt <= now) {
+      store.delete(key);
+    }
+  }
+}

 export function isPasswordResetRateLimited(email: string, ipHash: string | null) {
   const now = Date.now();
+  // Opportunistic cleanup on read
+  if (Math.random() < 0.01) cleanupExpiredAttempts();
   const attempt = attemptsStore().get(rateLimitKey(email, ipHash));
   return Boolean(attempt && attempt.resetAt > now && attempt.count >= RATE_LIMIT_MAX_ATTEMPTS);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/passwordReset.ts` around lines 118 - 144, The attempts Map in
attemptsStore can grow unbounded because expired entries are never removed; add
periodic cleanup to prune entries whose resetAt <= Date.now() (e.g., in
recordPasswordResetAttempt and/or isPasswordResetRateLimited) by scanning
attemptsStore() and deleting expired keys, but do it lazily and cheaply (run a
full scan only occasionally — e.g., probabilistically with Math.random() or
every N calls tracked by a small counter) so you avoid CPU spikes; reference
attemptsStore(), recordPasswordResetAttempt(), isPasswordResetRateLimited(), and
RATE_LIMIT_WINDOW_MS when implementing the cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@drizzle/password_reset_tokens_incremental.sql`:
- Around line 15-16: The ALTER TABLE ADD CONSTRAINT line for
password_reset_tokens_user_id_users_id_fk is not idempotent and will error if
the constraint already exists; replace it with an existence-guarded creation
(e.g. a small DO $$ BEGIN ... END $$; block or a conditional SELECT against
pg_constraint/pg_class/pg_namespace) that checks for the constraint name
"password_reset_tokens_user_id_users_id_fk" on table "password_reset_tokens" and
only executes the ADD CONSTRAINT ... FOREIGN KEY ("user_id") REFERENCES
"public"."users"("id") ON DELETE cascade ON UPDATE no action when absent,
preserving the exact constraint definition.

In `@e2e/fixtures/auth.fixture.ts`:
- Around line 170-174: The mock for POST "/api/auth/password-reset/confirm"
conflates short-password validation with token expiry; change the branching in
the handler (where pathname, method, state.resetToken and body are used) so that
if body.password is present but shorter than 8 you return a validation error
response (match the real endpoint's status/code and error shape) instead of the
generic "Reset link is invalid or expired", and only return the generic
token/expiry error when body.token does not match state.resetToken or
state.resetToken is missing; keep uses of json(route, ...) but adjust the status
and payload for the validation branch to mirror the real API.

In `@README.md`:
- Around line 86-88: The README and runtime disagree: the code in
src/lib/email.ts currently enables Resend based only on EMAIL_FROM and
RESEND_API_KEY, but the docs say EMAIL_PROVIDER=resend should be the switch;
update src/lib/email.ts to require EMAIL_PROVIDER === 'resend' (in addition to
validating EMAIL_FROM and RESEND_API_KEY) before enabling the Resend path (the
function/initializer that constructs the mail client or sendPasswordResetEmail
logic), or alternatively update the README to state that both EMAIL_FROM and
RESEND_API_KEY are required and that EMAIL_PROVIDER is not used; pick one
approach and make the change consistently and update comments/validation
messages accordingly.

In `@src/app/api/auth/password-reset/confirm/route.ts`:
- Around line 10-19: Wrap the call to await request.json() in a try/catch and
treat JSON parsing errors as a 400 client error: change the code around the
body/token/password extraction so that if request.json() throws (e.g., a
SyntaxError or other parse error) you return NextResponse.json({ error:
"Malformed JSON" }, { status: 400 }) instead of letting the outer catch convert
it to a 500; keep the existing validation for token and password (the token
trimming and password length check) after successful parsing.

In `@src/app/api/auth/password-reset/request/route.ts`:
- Around line 45-56: The current read-before-write check using recentCutoff and
activeToken against the passwordResetTokens table is race-prone: concurrent
requests can both see no active token and both insert new ones. Fix by making
issuance atomic for a given user: wrap the logic in a transaction and acquire a
per-user serialization point (e.g., SELECT FOR UPDATE on the user's row or an
advisory lock keyed by user.id) or atomically update existing tokens (set usedAt
or revoked) and then insert the new token in the same transaction; ensure you
reference passwordResetTokens.createdAt / usedAt and user.id when performing the
lock/update so no two transactions can both succeed.
- Around line 69-74: The issue: throwing from sendPasswordResetEmail makes
requests for existing emails return 500 while non-existing emails return the
normal non-enumeration message; fix by preserving the same response shape for
both cases. Wrap the call to sendPasswordResetEmail (in the route handler where
PASSWORD_RESET_REQUEST_MESSAGE is returned) in its own try/catch, log the
provider/send error (console.error or processLogger) but do not change the HTTP
response—always return NextResponse.json({ message:
PASSWORD_RESET_REQUEST_MESSAGE }) for both non-existing emails and send
failures; keep the outer catch for truly unexpected errors only.

In `@src/app/reset-password/page.tsx`:
- Around line 158-213: Wrap the inputs and button in a semantic <form> so the
flow uses form submission rather than onClick on a div; make the submit button
type="submit" and move the current onClick logic (calling submit or
requestReset) into a form onSubmit handler that calls event.preventDefault()
then calls submit() or requestReset() based on token. Add visible <label>
elements with htmlFor and matching id attributes for the email, password and
confirmPassword inputs (use the password, confirmPassword and email state
bindings), and keep the existing status/message rendering but add aria-live
(e.g., aria-live="polite" or "assertive" depending on status) to the message
container so screen readers are notified; preserve styling via
inputStyle/buttonStyle and keep disabling logic using status === "submitting".

In `@src/components/AuthScreen.tsx`:
- Around line 53-55: The resetHref variable in AuthScreen.tsx currently appends
the user's email to the query string (resetHref); remove the email from the URL
and always navigate to "/reset-password" to avoid leaking PII. Replace
occurrences that build `/reset-password?email=...` with a plain
"/reset-password", and instead pass the email securely (e.g., via in-memory
state/context, navigation state, or a POST/secure store) to prefill the reset
form; update any consumers of resetHref and the reset form to read email from
the chosen secure mechanism rather than the query string.

In `@src/lib/email.ts`:
- Around line 22-35: The fetch call that posts to
"https://api.resend.com/emails" must be wrapped with an AbortController-based
timeout: create an AbortController, pass controller.signal into fetch, start a
setTimeout that calls controller.abort() after a bounded period (e.g. 5–15s),
and clearTimeout once fetch returns; update the surrounding code that awaits the
response (where response is created and uses resendApiKey, fromAddress, to,
subject, text, html) to handle the abort/fetch rejection cleanly (propagate or
translate the error and ensure resources/timeouts are cleared).
- Around line 17-20: The current check using fromAddress and resendApiKey (in
the email sending flow) silently returns { delivered: false } in all
environments; change it so that when either fromAddress or resendApiKey is
missing and process.env.NODE_ENV === 'production' the code throws a clear Error
(e.g., "Email delivery not configured: EMAIL_FROM and RESEND_API_KEY required")
to fail fast in prod, while retaining the console.info + return { delivered:
false } behavior for non-production environments; update the conditional around
fromAddress and resendApiKey in the function that performs sending (referencing
those variables) to implement this branching.

---

Outside diff comments:
In `@ios/StillPointApp/ViewModels/AuthViewModel.swift`:
- Around line 5-75: Mark AuthViewModel as `@MainActor` to ensure main-thread
isolation for properties mutated across async boundaries (email, isSubmitting,
error, resetMessage, isRequestingPasswordReset), e.g. add `@MainActor` above the
final class AuthViewModel declaration; and in requestPasswordReset() capture and
assign the API-returned message to resetMessage by calling
APIClient.shared.requestPasswordReset(email: trimmedEmail) once and storing its
returned String instead of using the hardcoded message (refer to the
requestPasswordReset() method and APIClient.shared.requestPasswordReset).

In `@src/middleware.ts`:
- Around line 10-24: The allowlist in publicPaths plus the middleware check in
middleware(...) uses pathname.startsWith(p) which over-broadly matches prefixes
(e.g., "/api/auth/password-reset" will match "/api/auth/password-reset-extra");
change the matching to exact-path checks (or a Set/includes) or use anchored
regexes so only exact endpoints are allowed: update the publicPaths handling in
middleware to compare pathname === p (or test ^p$) for each entry in publicPaths
instead of startsWith, and ensure publicPaths entries list the specific
endpoints (e.g., "/api/auth/password-reset/request" and
"/api/auth/password-reset/confirm") so only those exact routes bypass auth.

---

Nitpick comments:
In @.env.example:
- Around line 18-21: The .env.example documents EMAIL_PROVIDER but the runtime
in src/lib/email.ts ignores it and always uses Resend (gated only by EMAIL_FROM
and RESEND_API_KEY); either remove/clarify EMAIL_PROVIDER from the example or
make src/lib/email.ts honor it. Fix by updating src/lib/email.ts to read
process.env.EMAIL_PROVIDER and branch the sending logic (e.g., support 'resend'
and 'console' modes), wiring the existing Resend client initialization
(RESEND_API_KEY, EMAIL_FROM) when EMAIL_PROVIDER === 'resend' and falling back
to the current console/log behavior when EMAIL_PROVIDER === 'console' (update
the sendEmail/sendPasswordResetEmail functions to consult EMAIL_PROVIDER).
Ensure env vars EMAIL_FROM and RESEND_API_KEY remain used for Resend mode and
update .env.example accordingly if you choose the doc-only path.

In `@ios/StillPointApp/ViewModels/AuthViewModel.swift`:
- Around line 68-70: The reset handler in AuthViewModel currently ignores the
string returned by APIClient.shared.requestPasswordReset(email:) and instead
assigns a hardcoded message to resetMessage; change it to capture and assign the
API's returned string (e.g., let message = try await
APIClient.shared.requestPasswordReset(email: trimmedEmail); resetMessage =
message) so resetMessage uses the API response as the single source of truth and
remove the duplicate hardcoded text.

In `@ios/StillPointApp/Views/AuthView.swift`:
- Around line 101-121: The reset confirmation Text currently shown by checking
vm.resetMessage can persist when the view switches to sign-up; restrict it to
login mode by rendering the Text only when vm.resetMessage is non-nil AND
vm.isSignUp is false (or move the entire vm.resetMessage Text block inside the
existing if !vm.isSignUp block). Update the view so the UI uses the combined
condition (e.g., vm.resetMessage != nil && !vm.isSignUp) to avoid showing
resetMessage during sign-up flows and keep the Button and its disabled/label
behavior unchanged.

In `@src/db/schema.ts`:
- Around line 34-42: The tokenHash column is declared unique via tokenHash:
varchar("token_hash", { length: 64 }).unique().notNull(), and you also create a
uniqueIndex("password_reset_tokens_token_hash_unique").on(table.tokenHash) —
remove the redundant unique index definition (the uniqueIndex call) so there is
only one uniqueness constraint on tokenHash; keep the column-level .unique() on
tokenHash (or if you prefer index-style uniqueness, remove .unique() and keep
the uniqueIndex) and update the table indexes block to omit the duplicate
uniqueIndex reference.

In `@src/lib/passwordReset.ts`:
- Around line 118-144: The attempts Map in attemptsStore can grow unbounded
because expired entries are never removed; add periodic cleanup to prune entries
whose resetAt <= Date.now() (e.g., in recordPasswordResetAttempt and/or
isPasswordResetRateLimited) by scanning attemptsStore() and deleting expired
keys, but do it lazily and cheaply (run a full scan only occasionally — e.g.,
probabilistically with Math.random() or every N calls tracked by a small
counter) so you avoid CPU spikes; reference attemptsStore(),
recordPasswordResetAttempt(), isPasswordResetRateLimited(), and
RATE_LIMIT_WINDOW_MS when implementing the cleanup.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7736e785-ee37-4bee-a9a0-743bd3497feb

📥 Commits

Reviewing files that changed from the base of the PR and between 9a42f11 and 3064513.

📒 Files selected for processing (18)
  • .env.example
  • README.md
  • drizzle/password_reset_tokens_incremental.sql
  • e2e/auth/password-reset.spec.ts
  • e2e/fixtures/auth.fixture.ts
  • ios/StillPointApp/ViewModels/AuthViewModel.swift
  • ios/StillPointApp/Views/AuthView.swift
  • ios/StillPointAppUITests/StillPointAppUITests.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
  • src/app/api/auth/password-reset/confirm/route.ts
  • src/app/api/auth/password-reset/request/route.ts
  • src/app/reset-password/page.tsx
  • src/components/AuthScreen.tsx
  • src/db/schema.ts
  • src/lib/email.ts
  • src/lib/passwordReset.ts
  • src/middleware.ts

Comment thread drizzle/password_reset_tokens_incremental.sql Outdated
Comment thread e2e/fixtures/auth.fixture.ts
Comment thread README.md Outdated
Comment thread src/app/api/auth/password-reset/confirm/route.ts Outdated
Comment thread src/app/api/auth/password-reset/request/route.ts Outdated
Comment thread src/app/api/auth/password-reset/request/route.ts Outdated
Comment thread src/app/reset-password/page.tsx Outdated
Comment thread src/components/AuthScreen.tsx Outdated
Comment thread src/lib/email.ts
Comment thread src/lib/email.ts
cursoragent and others added 5 commits April 25, 2026 15:28
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Copy link
Copy Markdown

@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.

🧹 Nitpick comments (4)
src/app/reset-password/page.tsx (1)

43-74: Consider clearing the email field on successful request.

After a successful reset link request, the email field retains its value. For UX consistency with the confirm flow (which clears password fields on success at lines 111-112), consider clearing the email field when status === "success".

Optional enhancement
       setStatus("success");
+      setEmail("");
       setMessage(data.message || "If an account exists for that email, a reset link will arrive shortly.");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/reset-password/page.tsx` around lines 43 - 74, After a successful
password-reset request, clear the email input for UX consistency: in the
requestReset function, after you call setStatus("success") and setMessage(...),
also call setEmail("") (or the state setter managing the email input) to reset
the field; ensure setEmail is in scope (the same state used to populate the
email variable) before adding this call.
src/lib/passwordReset.ts (2)

107-144: Rate limiting is process-local; document or mitigate production limitations.

The in-memory rate limiter (using globalThis) won't share state across serverless instances. The README (line 97) already mentions this limitation and recommends WAF throttling for production. This is acceptable given the documentation, but consider adding a code comment here for maintainability.

Add inline documentation
+/**
+ * In-memory rate limiting for password reset requests.
+ * NOTE: This is per-process and does not share state across serverless instances.
+ * Production deployments should rely on platform/WAF rate limiting for stronger protection.
+ */
 const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000;
 const RATE_LIMIT_MAX_ATTEMPTS = 5;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/passwordReset.ts` around lines 107 - 144, Add an inline comment near
the in-memory rate limiter definitions (globalForPasswordReset, attemptsStore,
RATE_LIMIT_WINDOW_MS, RATE_LIMIT_MAX_ATTEMPTS) explaining that the rate limiting
is process-local and will not be shared across serverless instances or multiple
servers; mention that isPasswordResetRateLimited and recordPasswordResetAttempt
rely on this process-local Map and recommend using a centralized store (Redis,
DB, or WAF throttling) for production to ensure global rate limiting and to
avoid relying solely on this in-memory mechanism.

81-105: consumePasswordResetToken is unused - extract token consumption logic to avoid duplication.

The helper function at lines 81-105 wraps the token consumption pattern, but src/app/api/auth/password-reset/confirm/route.ts (lines 34-45) duplicates this logic inline. Refactor the confirm route to call this helper and centralize the token consumption logic:

Refactor confirm route to use consumePasswordResetToken

In src/app/api/auth/password-reset/confirm/route.ts:

    const passwordHash = await hashPassword(password);
    const updated = await poolDb.transaction(async (tx) => {
-     const [resetToken] = await tx
-       .update(passwordResetTokens)
-       .set({ usedAt: new Date() })
-       .where(
-         and(
-           eq(passwordResetTokens.userId, resetPayload.userId),
-           eq(passwordResetTokens.tokenHash, resetPayload.tokenHash),
-           isNull(passwordResetTokens.usedAt),
-           gt(passwordResetTokens.expiresAt, new Date()),
-         ),
-       )
-       .returning({ userId: passwordResetTokens.userId });
-     if (!resetToken) {
+     const result = await consumePasswordResetToken(tx, token);
+     if (!result.ok) {
        return false;
      }
      await tx.update(users)
        .set({ passwordHash, updatedAt: new Date() })
-       .where(eq(users.id, resetToken.userId));
+       .where(eq(users.id, result.userId));
      return true;
    });

Don't forget to import consumePasswordResetToken from @/lib/passwordReset.

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

In `@src/lib/passwordReset.ts` around lines 81 - 105, The password reset token
consumption logic is duplicated in the confirm route; replace the inline logic
with a call to the existing helper consumePasswordResetToken(tx, token): import
consumePasswordResetToken from the passwordReset module, pass the same
transactional db instance (tx) and the provided token, then use its returned
shape ({ ok, userId }) to handle success/failure exactly as the inline code did
(return the same error responses when ok is false and continue flow when ok is
true). Ensure you remove the duplicated update/where block from the confirm
route and rely solely on consumePasswordResetToken for marking tokens as used
and retrieving userId.
src/lib/email.ts (1)

50-63: Consider HTML-escaping the link in the email body.

The link variable is constructed from user-controlled token and embedded directly into HTML at line 59. While the token is base64url-encoded JWT (safe characters), future changes could introduce XSS risk in email clients. HTML-escaping the href is a defensive measure.

Defensive escaping suggestion
+function escapeHtml(str: string) {
+  return str.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
+}

 export async function sendPasswordResetEmail({ to, token }: { to: string; token: string }) {
   const resetUrl = new URL("/reset-password", appUrl);
   resetUrl.searchParams.set("token", token);
   const link = resetUrl.toString();
+  const safeLink = escapeHtml(link);

   const result = await sendEmail({
     to,
     subject: "Reset your Still Point password",
     text: `Use this link to reset your Still Point password. It expires in ${PASSWORD_RESET_TTL_MINUTES} minutes:\n\n${link}\n\nIf you did not request this, you can ignore this email.`,
-    html: `<p>Use this link to reset your Still Point password. It expires in ${PASSWORD_RESET_TTL_MINUTES} minutes:</p><p><a href="${link}">Reset your password</a></p><p>If you did not request this, you can ignore this email.</p>`,
+    html: `<p>Use this link to reset your Still Point password. It expires in ${PASSWORD_RESET_TTL_MINUTES} minutes:</p><p><a href="${safeLink}">Reset your password</a></p><p>If you did not request this, you can ignore this email.</p>`,
     devLogMessage: `Password reset link for local development: ${link}`,
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/email.ts` around lines 50 - 63, The HTML email embeds the
user-controlled link directly in sendPasswordResetEmail (variable link) which
could allow XSS if token handling changes; update the function to HTML-escape
the link before inserting it into the html template (escape characters like &,
<, >, ", and ') by using an existing escapeHtml helper or adding one, then use
the escaped value in the <a href="..."> and any other HTML contexts (keep the
text/plain body unchanged). Ensure you reference sendPasswordResetEmail, link,
and the sendEmail call when making the change so the escaped value is passed
into sendEmail's html field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app/reset-password/page.tsx`:
- Around line 43-74: After a successful password-reset request, clear the email
input for UX consistency: in the requestReset function, after you call
setStatus("success") and setMessage(...), also call setEmail("") (or the state
setter managing the email input) to reset the field; ensure setEmail is in scope
(the same state used to populate the email variable) before adding this call.

In `@src/lib/email.ts`:
- Around line 50-63: The HTML email embeds the user-controlled link directly in
sendPasswordResetEmail (variable link) which could allow XSS if token handling
changes; update the function to HTML-escape the link before inserting it into
the html template (escape characters like &, <, >, ", and ') by using an
existing escapeHtml helper or adding one, then use the escaped value in the <a
href="..."> and any other HTML contexts (keep the text/plain body unchanged).
Ensure you reference sendPasswordResetEmail, link, and the sendEmail call when
making the change so the escaped value is passed into sendEmail's html field.

In `@src/lib/passwordReset.ts`:
- Around line 107-144: Add an inline comment near the in-memory rate limiter
definitions (globalForPasswordReset, attemptsStore, RATE_LIMIT_WINDOW_MS,
RATE_LIMIT_MAX_ATTEMPTS) explaining that the rate limiting is process-local and
will not be shared across serverless instances or multiple servers; mention that
isPasswordResetRateLimited and recordPasswordResetAttempt rely on this
process-local Map and recommend using a centralized store (Redis, DB, or WAF
throttling) for production to ensure global rate limiting and to avoid relying
solely on this in-memory mechanism.
- Around line 81-105: The password reset token consumption logic is duplicated
in the confirm route; replace the inline logic with a call to the existing
helper consumePasswordResetToken(tx, token): import consumePasswordResetToken
from the passwordReset module, pass the same transactional db instance (tx) and
the provided token, then use its returned shape ({ ok, userId }) to handle
success/failure exactly as the inline code did (return the same error responses
when ok is false and continue flow when ok is true). Ensure you remove the
duplicated update/where block from the confirm route and rely solely on
consumePasswordResetToken for marking tokens as used and retrieving userId.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 366e9156-154e-426f-92fe-54c4668fedaf

📥 Commits

Reviewing files that changed from the base of the PR and between 3064513 and a989afc.

📒 Files selected for processing (18)
  • .env.example
  • README.md
  • drizzle/password_reset_tokens_incremental.sql
  • e2e/auth/password-reset.spec.ts
  • e2e/fixtures/auth.fixture.ts
  • ios/StillPointApp/ViewModels/AuthViewModel.swift
  • ios/StillPointApp/Views/AuthView.swift
  • ios/StillPointAppUITests/StillPointAppUITests.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
  • src/app/api/auth/password-reset/confirm/route.ts
  • src/app/api/auth/password-reset/request/route.ts
  • src/app/reset-password/page.tsx
  • src/components/AuthScreen.tsx
  • src/db/schema.ts
  • src/lib/email.ts
  • src/lib/passwordReset.ts
  • src/middleware.ts
✅ Files skipped from review due to trivial changes (4)
  • src/middleware.ts
  • ios/StillPointShared/Sources/StillPointShared/DTOs/DTOs.swift
  • .env.example
  • src/db/schema.ts
🚧 Files skipped from review as they are similar to previous changes (6)
  • src/components/AuthScreen.tsx
  • ios/StillPointApp/Views/AuthView.swift
  • ios/StillPointShared/Sources/StillPointShared/APIClient.swift
  • drizzle/password_reset_tokens_incremental.sql
  • ios/StillPointApp/ViewModels/AuthViewModel.swift
  • ios/StillPointAppUITests/StillPointAppUITests.swift

Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Copy link
Copy Markdown

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/auth/password-reset.spec.ts`:
- Around line 22-27: The mock route in password-reset.spec.ts conflates token
expiration and password validation errors; update the handler that currently
checks body.token !== "valid-reset-token" || !body.password ||
body.password.length < 8 so that token failures still return the existing "Reset
link is invalid or expired" response (via route.fulfill) but short/malformed
passwords return the validation-specific error payload the real API returns
(e.g., a 4xx validation status and a JSON body indicating a password field
error). Locate the conditional in the test mock and split it into two branches:
one that checks token validity and one that checks password presence/length and
returns the validation error accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e5a5d4a9-158e-45ef-b55d-f8067a0280a6

📥 Commits

Reviewing files that changed from the base of the PR and between a989afc and 1614b9b.

📒 Files selected for processing (2)
  • e2e/auth/password-reset.spec.ts
  • src/app/reset-password/page.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/reset-password/page.tsx

Comment thread e2e/auth/password-reset.spec.ts Outdated
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (1)
src/app/reset-password/page.tsx (1)

169-197: Extract repeated visually-hidden label style into one constant.

The same inline hidden-label style is repeated three times; centralizing it will reduce duplication and keep future a11y tweaks consistent.

♻️ Suggested refactor
+const visuallyHiddenLabelStyle: React.CSSProperties = {
+  position: "absolute",
+  width: 1,
+  height: 1,
+  overflow: "hidden",
+};
...
-<label htmlFor="new-password" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden" }}>
+<label htmlFor="new-password" style={visuallyHiddenLabelStyle}>
...
-<label htmlFor="confirm-new-password" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden" }}>
+<label htmlFor="confirm-new-password" style={visuallyHiddenLabelStyle}>
...
-<label htmlFor="reset-email" style={{ position: "absolute", width: 1, height: 1, overflow: "hidden" }}>
+<label htmlFor="reset-email" style={visuallyHiddenLabelStyle}>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app/reset-password/page.tsx` around lines 169 - 197, Create a single
reusable constant (e.g., hiddenLabelStyle) for the visually-hidden label style
and replace the three inline label style objects with that constant; add the
constant near the top of the Reset Password component in
src/app/reset-password/page.tsx and update the labels with htmlFor values
"new-password", "confirm-new-password", and "reset-email" to use
hiddenLabelStyle so the style is centralized and no inline duplicates remain.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/passwordReset.ts`:
- Around line 48-57: The JWT created by createPasswordResetToken currently
includes email (PII) in its payload; remove email from the SignJWT payload so it
only embeds nonce (and any non-PII needed) and adjust the verification flow
(verifyPasswordResetToken / the code that reads the JWT) to stop expecting
email; then update the caller in the password-reset request route to call
createPasswordResetToken with only userId (remove passing email). Ensure all
references to the JWT payload within createPasswordResetToken and the
corresponding verify/consume function use only the nonce/userId and do not rely
on email.
- Around line 123-149: The attempts Map created by attemptsStore() never prunes
expired entries, causing unbounded memory growth; update the logic to remove
stale entries by either (a) adding a periodic cleanup (setInterval) that
iterates attemptsStore() and deletes entries with resetAt <= Date.now(), or (b)
pruning on access inside recordPasswordResetAttempt() and
isPasswordResetRateLimited() by scanning and deleting expired keys before
reading/updating; use RATE_LIMIT_WINDOW_MS/resetAt to determine expiry and keep
the existing
rateLimitKey()/attemptsStore()/recordPasswordResetAttempt()/isPasswordResetRateLimited()
functions as the hook points for this cleanup so the Map stays bounded.

---

Nitpick comments:
In `@src/app/reset-password/page.tsx`:
- Around line 169-197: Create a single reusable constant (e.g.,
hiddenLabelStyle) for the visually-hidden label style and replace the three
inline label style objects with that constant; add the constant near the top of
the Reset Password component in src/app/reset-password/page.tsx and update the
labels with htmlFor values "new-password", "confirm-new-password", and
"reset-email" to use hiddenLabelStyle so the style is centralized and no inline
duplicates remain.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f5e5ee83-fed2-45ea-85ba-e1bf1a7549bb

📥 Commits

Reviewing files that changed from the base of the PR and between 1614b9b and 3c40623.

📒 Files selected for processing (4)
  • e2e/auth/password-reset.spec.ts
  • src/app/reset-password/page.tsx
  • src/lib/email.ts
  • src/lib/passwordReset.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • e2e/auth/password-reset.spec.ts

Comment thread src/lib/passwordReset.ts
Comment thread src/lib/passwordReset.ts
Co-authored-by: Bretton Auerbach <auerbachb@users.noreply.github.com>
@auerbachb auerbachb merged commit 5c6ae1a into main Apr 26, 2026
13 checks passed
@auerbachb auerbachb deleted the cursor/password-reset-ad4b branch April 26, 2026 01:05
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