feat(auth): sign-up, sign-in and password-reset flow#8
Conversation
Build the full authentication experience from the Figma designs as a feature-first set of routes under (auth), backed by the existing better-auth client. Screens: - Sign up: choose path (email + Google/Apple/Github), with email, verify code - Sign in: choose path, email + password (forgot link, show/hide), verify code - Reset password: request code, verify code, set new password Adds a shared auth shell (star-field background + brand glow, centered logo, terms footer) and reusable building blocks: AuthCard, AuthInput, OtpInput, SocialAuthButtons, OrDivider, AuthFooterPill, and the step forms. Wires emailOtp (passwordless sign-up + verification), signIn.email, signIn.social and emailOtp.resetPassword. Replaces the previous basic sign-in page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
More reviews will be available in 55 minutes and 7 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more credits in the billing tab to continue. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the 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 include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (23)
📝 WalkthroughWalkthroughThis PR introduces a complete authentication system with shared layout, reusable auth UI components, and three parallel email-based flows (sign-in with password fallback, sign-up with OTP, password reset with OTP). It includes social authentication buttons, OTP input/verification components, helper utilities for safe redirects and number formatting, and enhancements to existing UI components like Input and AppNav. ChangesAuthentication System Implementation
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (2)
src/features/auth/components/otp-input.tsx (1)
80-99: ⚡ Quick winAdd accessible labeling for screen readers.
Individual digit inputs without labels can be confusing for assistive technology users. Consider adding
aria-labelto each input indicating its position (e.g., "Digit 1 of 6").♿ Proposed accessibility improvement
<input key={index} ref={el => { refs.current[index] = el; }} type='text' inputMode='numeric' autoComplete='one-time-code' maxLength={1} disabled={disabled} autoFocus={autoFocus && index === 0} value={value[index] ?? ''} onChange={event => handleChange(event, index)} onKeyDown={event => handleKeyDown(event, index)} onPaste={handlePaste} + aria-label={`Digit ${index + 1} of ${length}`} className={cn(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/auth/components/otp-input.tsx` around lines 80 - 99, Add an accessible aria-label to each OTP input so screen readers can announce its position: in the input JSX (the element that uses refs.current[index], value[index], onChange -> handleChange, onKeyDown -> handleKeyDown, onPaste -> handlePaste) add an aria-label prop like aria-label={`Digit ${index + 1} of ${totalDigits ?? value?.length ?? refs.current.length}`} (or derive totalDigits from the prop that controls OTP length). Ensure the label updates per index and keep existing handlers/props intact.src/features/auth/components/otp-verify-form.tsx (1)
59-59: 💤 Low valueEncode OTP in URL for robustness.
While OTPs are typically numeric, for consistency and safety the OTP should also be URL-encoded.
🔧 Proposed fix
router.push( - `/reset-password/new?email=${encodeURIComponent(email)}&otp=${otp}` + `/reset-password/new?email=${encodeURIComponent(email)}&otp=${encodeURIComponent(otp)}` );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/features/auth/components/otp-verify-form.tsx` at line 59, The OTP is not URL-encoded in the reset link generation in otp-verify-form.tsx; update the template string that builds `/reset-password/new?email=${encodeURIComponent(email)}&otp=${otp}` to encode the OTP as well (use encodeURIComponent(otp)) so both email and otp are safely escaped when constructing the URL in the component that generates the reset link.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/app/`(auth)/reset-password/verify/page.tsx:
- Around line 12-30: The page currently defaults email to '' and renders the
verify UI even when no email is provided; add a guard that redirects to
'/reset-password' when email is missing. Import and call Next.js's redirect
(from 'next/navigation') or your app's router redirect function at the top of
the component (before rendering AuthCard/OtpVerifyForm) and perform if (!email)
redirect('/reset-password'); so OtpVerifyForm (mode='reset') is only rendered
when a valid email exists.
In `@src/app/`(auth)/sign-in/page.tsx:
- Around line 21-27: The sign-in redirect query param is being dropped: update
the Link to `/sign-in/email` to include the current `redirect` query value
(e.g., append `?redirect=${redirect}`) and propagate the same param into the
social sign-in branch by passing it into the SocialAuthButtons component (add a
prop such as `redirect` or `callbackUrl` and use it in SocialAuthButtons to set
the OAuth callback/next URL instead of the hardcoded `/dashboard`). Locate the
Link in page.tsx and the SocialAuthButtons usage and ensure they both read the
current search param (via useSearchParams or equivalent) and forward it so the
original redirect is preserved across both paths.
In `@src/app/`(auth)/sign-in/verify/page.tsx:
- Around line 12-30: The verify page currently defaults email to '' and always
renders OtpVerifyForm, which can never succeed without an email; update the page
component to guard when email is missing by checking the derived email (the
email variable from searchParams) before rendering OtpVerifyForm/AuthCard: if
email is falsy, either redirect back to '/sign-in/email' or render a friendly
message/UI prompting the user to provide an email (e.g., replace OtpVerifyForm
with a link/button to backHref); ensure the check happens before rendering
OtpVerifyForm (referencing the email variable and the OtpVerifyForm and AuthCard
components) so no verification is attempted with an empty email.
In `@src/app/`(auth)/sign-up/verify/page.tsx:
- Around line 12-30: The page currently defaults email to '' and renders
OtpVerifyForm which will call authClient.signIn.emailOtp with an empty email;
update the component that uses searchParams and the email constant to
validate/guard the email before rendering OtpVerifyForm (e.g., check that email
is non-empty and a valid email format), and if the guard fails, redirect back to
the email entry route or render a friendly message/CTA (e.g., link to
'/sign-up/email') instead of rendering OtpVerifyForm; ensure the guard lives in
the verify page where email is read (the email variable derived from
searchParams) so OtpVerifyForm only receives a guaranteed valid email.
In `@src/features/auth/components/auth-input.tsx`:
- Around line 22-30: Replace the raw <input> in the AuthInput component with the
shared Input primitive: import the Input component and use <Input ref={ref}
{...props} className={cn('h-14 w-full rounded-md border ...
aria-invalid:border-error-500', hasAdornment && 'pr-12', className)} /> so you
preserve ref forwarding, {...props} forwarding, the cn class merge, and the
hasAdornment/pr-12 behavior; ensure the component still exposes the same props
and aria-invalid styling and remove the old native <input> element.
In `@src/features/auth/components/otp-input.tsx`:
- Around line 26-30: setDigit currently does value.split('') then assigns
next[index], which creates sparse arrays when index >= next.length; fix by
creating a fully populated array of the expected length before assigning: build
next as an array of length `length` filled with empty strings and copy existing
characters from `value` into it (or push empty strings until next.length ===
length), then set next[index] = digit and call onChange(next.join('').slice(0,
length)); reference the setDigit function and the variables value, length, next,
and onChange.
In `@src/features/auth/components/otp-verify-form.tsx`:
- Around line 56-60: The current router.push call that navigates in the reset
flow (the branch where mode === 'reset') leaks the OTP via URL
(`router.push(`/reset-password/new?email=${encodeURIComponent(email)}&otp=${otp}`)`);
change this to persist the OTP in a short-lived client-side or server-side state
(e.g., sessionStorage key like "resetOtp:<email>" or set a secure, short-lived
cookie) and navigate without the otp query param (only include the encoded email
or a state token), then read and immediately remove the OTP in the next
component before calling emailOtp.resetPassword; update the logic around
router.push, the reset-password/new component, and any emailOtp.resetPassword
invocation to use the stored OTP instead of the URL.
In `@src/features/auth/components/sign-in-email-form.tsx`:
- Around line 35-55: The redirect value read into redirectTo from searchParams
must be validated before calling router.push to prevent open redirects; update
the onSubmit flow to validate the redirect (the variable redirectTo derived from
searchParams.get('redirect')) and only navigate if it is a safe internal
path—e.g., allow only strings that begin with a single '/' and not '//' or full
URLs (or parse with the URL constructor and ensure same-origin), otherwise fall
back to '/dashboard'; make this validation check immediately before router.push
in the onSubmit handler (or compute a safeRedirect helper used by onSubmit) so
signIn.email and router.push use a sanitized path.
In `@src/features/auth/components/social-auth-buttons.tsx`:
- Around line 33-42: The handleSocial function sets pending then awaits
signIn.social but lacks error handling, so if signIn.social throws the pending
state never resets; wrap the await in try/catch/finally: call
setPending(provider) before trying, in catch handle thrown errors by calling
toast.error with the error message (falling back to the existing message), and
ensure setPending(null) runs in finally so buttons are re-enabled; refer to
handleSocial, setPending, signIn.social, and toast.error to locate and update
the code.
---
Nitpick comments:
In `@src/features/auth/components/otp-input.tsx`:
- Around line 80-99: Add an accessible aria-label to each OTP input so screen
readers can announce its position: in the input JSX (the element that uses
refs.current[index], value[index], onChange -> handleChange, onKeyDown ->
handleKeyDown, onPaste -> handlePaste) add an aria-label prop like
aria-label={`Digit ${index + 1} of ${totalDigits ?? value?.length ??
refs.current.length}`} (or derive totalDigits from the prop that controls OTP
length). Ensure the label updates per index and keep existing handlers/props
intact.
In `@src/features/auth/components/otp-verify-form.tsx`:
- Line 59: The OTP is not URL-encoded in the reset link generation in
otp-verify-form.tsx; update the template string that builds
`/reset-password/new?email=${encodeURIComponent(email)}&otp=${otp}` to encode
the OTP as well (use encodeURIComponent(otp)) so both email and otp are safely
escaped when constructing the URL in the component that generates the reset
link.
🪄 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: CHILL
Plan: Pro
Run ID: aaa4ee73-52ad-42cb-a2e9-39365fbf6a65
📒 Files selected for processing (25)
src/app/(auth)/layout.tsxsrc/app/(auth)/reset-password/new/page.tsxsrc/app/(auth)/reset-password/page.tsxsrc/app/(auth)/reset-password/verify/page.tsxsrc/app/(auth)/sign-in/email/page.tsxsrc/app/(auth)/sign-in/page.tsxsrc/app/(auth)/sign-in/verify/page.tsxsrc/app/(auth)/sign-up/email/page.tsxsrc/app/(auth)/sign-up/page.tsxsrc/app/(auth)/sign-up/verify/page.tsxsrc/features/auth/components/auth-card.tsxsrc/features/auth/components/auth-footer-pill.tsxsrc/features/auth/components/auth-input.tsxsrc/features/auth/components/auth-terms.tsxsrc/features/auth/components/or-divider.tsxsrc/features/auth/components/otp-input.tsxsrc/features/auth/components/otp-verify-form.tsxsrc/features/auth/components/provider-icons.tsxsrc/features/auth/components/reset-password-form.tsxsrc/features/auth/components/reset-password-new-form.tsxsrc/features/auth/components/sign-in-email-form.tsxsrc/features/auth/components/sign-in-form.tsxsrc/features/auth/components/sign-up-email-form.tsxsrc/features/auth/components/social-auth-buttons.tsxsrc/features/auth/index.ts
💤 Files with no reviewable changes (1)
- src/features/auth/components/sign-in-form.tsx
Rebuild components/ui/input.tsx as the full Figma Input component: label + bordered frame (leading icon, control, trailing add-on / icon) + helper text, composed via inputSize (small | large), shape (rounded | pill) and state (default | success | error). Hover, active/typing and filled are handled by CSS (:hover, :focus-within, value); read-only and disabled by the native attributes. Covers the 32-variant matrix. Adopt it across the auth forms (sign-up, sign-in, reset, set-new-password), removing the bespoke AuthInput. Update the one other consumer (filter-rail) to inputSize=small and add an /input-preview matrix page. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Guard the verify and set-password pages: redirect to the entry route when the email query param is missing, so OTP verification is never attempted with an empty email. - Preserve the `redirect` param across the sign-in choose-path: forward it to the email link and as the social callbackURL. - Validate `redirect` with a safeRedirect helper (reject //, /\ and absolute URLs) to prevent open redirects. - Stop leaking the reset OTP in the URL: carry it via sessionStorage and clear it after use. - Fix OtpInput sparse-array write on non-sequential entry; add per-digit aria-labels. - Handle thrown errors in social sign-in so the pending state always resets. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Validate auth forms on touch (mode: 'onTouched') so the input error state triggers on blur and updates live as the user types, instead of only on submit. - Add micro-interactions to the Input: eased multi-property transitions, a focus glow ring tinted per state (primary / success / error), smooth icon color transitions, and a fade/zoom entrance for the success/error status icon (motion-reduce safe). - Apply the same polish to the OTP boxes: transitions, primary caret, focus tint, and a group border highlight on focus. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Implements the full authentication experience from the Figma designs as a feature-first set of routes under
(auth), wired to the existing better-auth client. Replaces the previous basic/sign-inpage.Screens (8)
Shared shell & building blocks (
src/features/auth/)(auth)/layout.tsxreuses marketing'sHeroBackground(star-field + bottom brand glow), centeredBoundlessLogo, and a terms line pinned to the bottom.AuthCard(plain +sectionedfor OTP),AuthInput,OtpInput(6-digit, two connected groups, auto-advance / backspace / paste),SocialAuthButtons,OrDivider,AuthFooterPill, and the step forms.Buttonprimitive; inputs use theForm/Inputprimitives and design tokens.Auth wiring
emailOtp.sendVerificationOtp/signIn.emailOtp(passwordless, auto-creates).signIn.email; social ->signIn.social.emailOtp.sendVerificationOtp({ type: 'forget-password' })thenemailOtp.resetPassword.Intentional deviations from Figma (flagged)
/sign-up/emailreads "Already have an account? Sign In" (Figma showed "Don't have an account? Sign up")./reset-password/new) has no Figma frame yet, so it follows the existing auth design system. Worth a design review when the official mockup lands.Verification
npm run typecheck,npm run lint(0 errors),npm run test(14/14) all pass.Note: the get-started modal and other unrelated in-progress changes were intentionally left out of this branch to keep the PR focused on auth.
Summary by CodeRabbit
Release Notes
New Features
Style