Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
0ed825a
refactor(better-auth): login/signup
jog1t Apr 7, 2026
3b2bd3b
feat(frontend): add feature flags module, remove __APP_TYPE__ vite in…
jog1t Apr 8, 2026
f012709
feat(frontend): migrate auth, utils, __root to feature flags
jog1t Apr 8, 2026
4ff9cfd
fix(frontend): fix inverted comment in shouldRetryAllExpect403
jog1t Apr 8, 2026
cc76956
feat(frontend): migrate onboarding, engine-data-provider, getting-sta…
jog1t Apr 8, 2026
1f4ea7a
feat(frontend): migrate support, branding, datacenter to feature flags
jog1t Apr 8, 2026
1e230c5
fix(frontend): use features.support (not features.branding) for onboa…
jog1t Apr 8, 2026
9160d5f
feat(frontend): migrate namespaceManagement flag
jog1t Apr 8, 2026
9ec8cb5
feat(frontend): migrate multitenancy flag — routing layer
jog1t Apr 8, 2026
781f56f
feat(frontend): migrate multitenancy flag — data providers and compon…
jog1t Apr 8, 2026
553dc1a
fix(frontend): remove as any cast and IIFE in actor-builds-list and t…
jog1t Apr 8, 2026
404e52e
feat(frontend): migrate layout and actors-list to feature flags, comp…
jog1t Apr 8, 2026
83a6c4b
fix(frontend): gate billing nav links behind features.billing (not ju…
jog1t Apr 8, 2026
98b609d
fix(frontend): fix actor-builds-list navigate types and migrate token…
jog1t Apr 8, 2026
852f7a1
refactor(frontend): flatten _engine layout route — move ns routes up …
jog1t Apr 8, 2026
c7610e7
fix(frontend): import useDialog from @/app/use-dialog in _context.tsx
jog1t Apr 8, 2026
d096dff
refactor(frontend): flatten _cloud layout route — move orgs routes up…
jog1t Apr 8, 2026
4494f87
refactor(frontend): update from: route references after _cloud/_engin…
jog1t Apr 8, 2026
d8a8983
refactor(frontend): collapse dev:cloud/dev:engine into single vite co…
jog1t Apr 8, 2026
8428302
fix(frontend): set base to / when multitenancy is enabled, /ui otherwise
jog1t Apr 8, 2026
3fd8c51
fix(frontend): multitenancy requires auth — enforce dependency in fea…
jog1t Apr 8, 2026
2acabf0
fix(frontend): gate billing metric queries behind features.billing
jog1t Apr 8, 2026
e9b13a6
fix(frontend): gate LazyBillingPlanBadge in context-switcher behind f…
jog1t Apr 8, 2026
4be9581
feat(frontend): add captcha feature flag and VITE_APP_TURNSTILE_SITE_…
jog1t Apr 8, 2026
03aa3ea
feat(frontend): add TurnstileWidget component
jog1t Apr 8, 2026
805866b
feat(frontend): wire Turnstile captcha into login form
jog1t Apr 8, 2026
7d8403c
feat(frontend): wire Turnstile captcha into sign-up form
jog1t Apr 8, 2026
a3a2790
fix(frontend): fix captcha token lifecycle, env var guard, and timeou…
jog1t Apr 8, 2026
93686df
cleanup
jog1t Apr 8, 2026
13d1c62
docs: add better-auth missing flows design spec
jog1t Apr 8, 2026
fe440a1
docs: add better-auth missing flows implementation plan
jog1t Apr 8, 2026
6a4fcd6
feat(frontend): add email verification flow
jog1t Apr 8, 2026
5cedd24
feat(frontend): add reset password flow
jog1t Apr 8, 2026
5712a88
feat(frontend): add org members management dialog
jog1t Apr 8, 2026
4ece873
feat(frontend): add org invitation acceptance landing page
jog1t Apr 8, 2026
a0549e2
feat(frontend): gate dashboard on emailVerified, add verify-email-pen…
jog1t Apr 9, 2026
d566165
fix(frontend): add toast feedback to resend verification email button
jog1t Apr 9, 2026
1851c2c
fix(frontend): show retry-after duration on 429 for resend verificati…
jog1t Apr 9, 2026
3269049
fix(frontend): use formatDuration for retry-after on 429
jog1t Apr 9, 2026
c77c84c
fix(frontend): countdown on rate-limit, fix isPending not resetting o…
jog1t Apr 9, 2026
bb4e043
features
jog1t Apr 9, 2026
8d57894
fix(frontend): add captcha to forgot password form
jog1t Apr 9, 2026
a5a5d0e
feat(frontend): add success toast after password reset
jog1t Apr 9, 2026
321e433
fix(frontend): sign out user when navigating back to sign in from ver…
jog1t Apr 9, 2026
b70ae29
fix(frontend): fix org members dialog layout and remove role picker
jog1t Apr 9, 2026
2326606
fix(frontend): make org member invite submit on enter
jog1t Apr 9, 2026
2338021
fix(frontend): show owner tag inline, remove role column from members…
jog1t Apr 9, 2026
c01b576
fix(frontend): remove role column from pending invitations table
jog1t Apr 9, 2026
0350de1
fix(frontend): add You badge next to current user in members list
jog1t Apr 9, 2026
0a6f71d
org members
jog1t Apr 9, 2026
24ccaf2
feat(frontend): identify user in Sentry and PostHog after login
jog1t Apr 9, 2026
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
162 changes: 162 additions & 0 deletions .agent/specs/2026-04-08-better-auth-missing-flows-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Better-Auth Missing Flows — Design Spec

**Date:** 2026-04-08

## Overview

After migrating from Clerk to better-auth, four auth/org flows are missing. This spec covers all four as independent, parallelizable implementation tasks.

## Shared Patterns

All new UI follows existing conventions:
- Routes in `frontend/src/routes/`
- Page components in `frontend/src/app/`
- Dialogs as lazy-loaded frames in `frontend/src/app/dialogs/`, registered in `frontend/src/app/use-dialog.tsx`
- Forms in `frontend/src/components/forms/` using controlled `Form` wrappers with `RootError`
- `authClient` from `frontend/src/lib/auth.ts` (better-auth React client with `organizationClient` plugin)
- Cards styled with `Card`, `CardHeader`, `CardContent`, `CardFooter` from `@/components/ui/card`
- Auth routes guard with `features.auth` check and redirect to `/login` if unauthenticated

---

## Task 1: Email Verification Flow

### Scope
- Post-sign-up success state in `SignUp` component
- New route `/verify-email` for the email link landing page

### Sign-up success state
After `authClient.signUp.email()` succeeds, replace the form content inline with a "Check your inbox" message inside the same `Card`. Add a `verified` state boolean to `SignUp`. When `true`, swap `<Form>` children for a static message:

```
Title: "Check your email"
Body: "We sent a verification link to <email>. Click it to activate your account."
Footer: "Resend email" (calls authClient.sendVerificationEmail) + "Back to sign in" link
```

OAuth sign-up (`handleGoogleSignUp`) skips this — redirect immediately as before.

### `/verify-email` route
File: `frontend/src/routes/verify-email.tsx`
Component file: `frontend/src/app/verify-email.tsx`

- Reads `?token=` from search params
- On mount calls `authClient.verifyEmail({ query: { token } })`
- Three UI states:
- Loading: spinner
- Success: "Email verified! Redirecting…" then `redirectToOrganization()`
- Error: "This link is invalid or has expired." + "Request a new link" button (calls `authClient.sendVerificationEmail` if session exists, else links to `/join`)
- No auth required to load this route (link arrives in email before login)

---

## Task 2: Reset Password Flow

### Scope
- "Forgot password?" link on the login page
- New route `/forgot-password` — email input form
- New route `/reset-password` — new password form (token from URL)

### Login page change
Add a small "Forgot password?" link below the `<PasswordField />` in `login.tsx`, linking to `/forgot-password`.

### `/forgot-password` route
File: `frontend/src/routes/forgot-password.tsx`
Component: `frontend/src/app/forgot-password.tsx`
Form: `frontend/src/components/forms/forgot-password-form.tsx`

Card layout matching `/login`:
```
Title: "Reset your password"
Body: EmailField + RootError
Footer: Submit "Send reset link" + "Back to sign in" link
```

On submit: `authClient.forgetPassword({ email, redirectTo: window.location.origin + "/reset-password" })`

After success, swap form content inline for:
```
"Reset link sent. Check your inbox."
```

### `/reset-password` route
File: `frontend/src/routes/reset-password.tsx`
Component: `frontend/src/app/reset-password.tsx`
Form: `frontend/src/components/forms/reset-password-form.tsx`

Reads `?token=` from search params. Card layout:
```
Title: "Choose a new password"
Body: PasswordField (label "New password") + PasswordField (label "Confirm password") + RootError
Footer: Submit "Set new password"
```

On submit: validates passwords match client-side, calls `authClient.resetPassword({ newPassword, token })`.

On success: redirect to `/login` with a success toast/message.
On error (expired token): show error with "Request a new link" → `/forgot-password`.

---

## Task 3: Org Members Dialog

### Scope
- New dialog frame `org-members-frame.tsx`
- "Manage Members" entry in user dropdown
- Registered in `useDialog`

### Dialog frame
File: `frontend/src/app/dialogs/org-members-frame.tsx`

Three sections:

**Members list** — calls `authClient.organization.getMembers({ query: { organizationId } })`. Each row: avatar, name/email, role badge, "Remove" button (calls `authClient.organization.removeMember`). Current user's row has no Remove button.

**Invite member form** — inline form below the list:
```
EmailField + role select (owner | admin | member, default member) + "Send Invite" button
```
Calls `authClient.organization.inviteMember({ email, role, organizationId })`.

**Pending invitations** — calls `authClient.organization.listInvitations({ query: { organizationId } })`. Each row: email, role, "Revoke" button (calls `authClient.organization.cancelInvitation`). Hidden section if empty.

### User dropdown change
In `frontend/src/app/user-dropdown.tsx`, add a "Manage Members" `DropdownMenuItem` above "Logout" (only when `params.organization` exists). Opens dialog via `navigate` with `modal: "org-members"` search param, following the same pattern as other modals.

### `useDialog` registration
Add `OrgMembers: createDialogHook(() => import("@/app/dialogs/org-members-frame"))` to `frontend/src/app/use-dialog.tsx`.

Add `"org-members"` to the modal enum in `frontend/src/routes/_context.tsx` and render `<OrgMembersDialog>` in `CloudModals`.

---

## Task 4: Org Invitation Landing Page

### Scope
- New route `/accept-invitation` — landing page for invited users

### Route
File: `frontend/src/routes/accept-invitation.tsx`
Component: `frontend/src/app/accept-invitation.tsx`

Reads `?invitationId=` from search params (better-auth puts this in the link).

**Auth-aware rendering:**
- If user is not logged in: show a card with the org name (fetched from the invitation details if the API allows unauthenticated lookup, otherwise just a generic message), with "Sign in to accept" and "Create account to accept" buttons. After auth, the user returns to this URL (pass `callbackURL` to social sign-in; redirect `from` for email sign-in).
- If user is logged in: show "You've been invited to join [Org Name]" with "Accept" and "Decline" buttons.

On accept: `authClient.organization.acceptInvitation({ invitationId })` → redirect to org.
On decline: `authClient.organization.rejectInvitation({ invitationId })` → redirect to `/`.
On error (expired/invalid): show error message with link to contact org admin.

No `beforeLoad` auth guard — the page must render for unauthenticated users.

---

## Implementation Notes

- Each task is fully independent and can be assigned to a separate agent.
- All new routes should have `features.auth` guards where appropriate (Tasks 1, 2 public; Tasks 3, 4 see spec above).
- Use `authClient.useSession()` in React components for reactive session state.
- No new dependencies required — better-auth's `organizationClient` plugin already includes all needed methods.
- Invitation list/revoke methods: confirm exact API names against `better-auth@1.5.6` docs before implementing (`authClient.organization.listInvitations`, `authClient.organization.cancelInvitation`).
Loading
Loading