-
Notifications
You must be signed in to change notification settings - Fork 70
feat: add /settings page for recovery flows #458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
ben-fornefeld
merged 5 commits into
main
from
accountpassword-recovery-flow-doesnt-work-en-1116
Jun 24, 2026
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
fd73aba
feat: add /settings page for recovery flows
drankou 13a3d14
add reveal option for new password
drankou 64509bd
improve post recovery link and clean up session
drankou 9861ce2
fix signout redirect
drankou cf54c57
Merge branch 'main' into accountpassword-recovery-flow-doesnt-work-en…
ben-fornefeld File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| import 'server-only' | ||
|
|
||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { AUTH_URLS } from '@/configs/urls' | ||
| import { handleCredentialChangeSuccess } from '@/core/server/auth' | ||
| import { clearAppSessionCookies } from '@/core/server/auth/ory/clear-session-cookies' | ||
|
|
||
| // Post-recovery password reset on /settings completed. The Kratos session minted | ||
| // by the recovery flow is still live, so a bare redirect to /sign-in lets Hydra | ||
| // silently mint tokens off it without a password prompt. Revoke every session | ||
| // for the identity (this device plus any other live session, e.g. a takeover's | ||
| // session elsewhere) and clear cookies before sending the user to /sign-in to | ||
| // authenticate with the new password. | ||
| export async function GET(request: NextRequest) { | ||
| await handleCredentialChangeSuccess() | ||
|
|
||
| const response = NextResponse.redirect( | ||
| new URL(AUTH_URLS.SIGN_IN, request.nextUrl.origin) | ||
| ) | ||
| clearAppSessionCookies(request, response) | ||
| return response | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,15 +1,49 @@ | ||
| 'use client' | ||
|
|
||
| import type { OryNodeInputProps } from '@ory/elements-react' | ||
| import { useState } from 'react' | ||
| import { EyeClosedIcon, EyeOpenIcon } from '@/ui/primitives/icons' | ||
| import { Input } from '@/ui/primitives/input' | ||
|
|
||
| export function OryInput({ inputProps, node }: OryNodeInputProps) { | ||
| const isPassword = inputProps.type === 'password' | ||
| const [revealed, setRevealed] = useState(false) | ||
|
|
||
| const placeholder = | ||
| node.attributes.name === 'identifier' || inputProps.type === 'email' | ||
| ? 'you@example.com' | ||
| : inputProps.type === 'password' | ||
| : isPassword | ||
| ? '••••••••••••' | ||
| : undefined | ||
|
|
||
| return <Input {...inputProps} {...(placeholder ? { placeholder } : {})} /> | ||
| const input = ( | ||
| <Input | ||
| {...inputProps} | ||
| {...(placeholder ? { placeholder } : {})} | ||
| {...(isPassword | ||
| ? { type: revealed ? 'text' : 'password', className: 'pr-8' } | ||
| : {})} | ||
| /> | ||
| ) | ||
|
|
||
| if (!isPassword) return input | ||
|
|
||
| return ( | ||
| <div className="relative w-full"> | ||
| {input} | ||
| <button | ||
| type="button" | ||
| aria-label={revealed ? 'Hide password' : 'Show password'} | ||
| aria-pressed={revealed} | ||
| onClick={() => setRevealed((value) => !value)} | ||
| className="text-fg-tertiary hover:text-fg absolute top-1/2 right-2 flex size-5 -translate-y-1/2 cursor-pointer items-center justify-center" | ||
| > | ||
| {revealed ? ( | ||
| <EyeClosedIcon className="size-4" /> | ||
| ) : ( | ||
| <EyeOpenIcon className="size-4" /> | ||
| )} | ||
| </button> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { ALLOW_SEO_INDEXING } from '@/configs/env-flags' | ||
| import { METADATA } from '@/configs/metadata' | ||
| import { AUTH_URLS } from '@/configs/urls' | ||
| import { buttonVariants } from '@/ui/primitives/button' | ||
|
|
||
| export const metadata = { | ||
| title: METADATA.title, | ||
| description: METADATA.description, | ||
| robots: ALLOW_SEO_INDEXING ? 'index, follow' : 'noindex, nofollow', | ||
| } | ||
|
|
||
| // Shell-less: no sidebar/team chrome, so the page renders with only a Kratos | ||
| // session (the post-recovery password reset has no e2b_session yet). | ||
| export default function SettingsLayout({ | ||
| children, | ||
| }: { | ||
| children: React.ReactNode | ||
| }) { | ||
| return ( | ||
| <div className="relative flex min-h-svh flex-col"> | ||
| <header className="bg-bg/40 sticky top-0 z-50 flex items-center justify-between gap-3 border-b px-4 py-4 backdrop-blur-md md:px-6"> | ||
| <h1 className="truncate">Account</h1> | ||
| <a | ||
| href={`${AUTH_URLS.SIGN_OUT}?return_to=${encodeURIComponent(AUTH_URLS.SIGN_IN)}`} | ||
| className={`${buttonVariants({ variant: 'secondary' })} shrink-0`} | ||
| > | ||
| Sign out | ||
| </a> | ||
| </header> | ||
| <div className="flex w-full flex-1 justify-center px-4 py-8"> | ||
| <div className="w-full max-w-2xl">{children}</div> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| import { getSettingsFlow, type OryPageParams } from '@ory/nextjs/app' | ||
| import oryConfig from '@/configs/ory' | ||
| import { getUserProfile } from '@/core/server/auth' | ||
| import { getOryConfigForRequest } from '@/core/server/auth/ory/request-config' | ||
| import { SettingsCards } from './settings-cards' | ||
|
|
||
| export const dynamic = 'force-dynamic' | ||
|
|
||
| // Ory-driven settings page, intentionally separate from /dashboard/account. | ||
| // It needs only a Kratos session (getSettingsFlow + getUserProfile read the | ||
| // session/identity, not e2b_session) — so the post-recovery password reset works | ||
| // before any Hydra token exists. Name/e-mail are shown read-only for reference; | ||
| // editing the account profile stays on the gated /dashboard/account page. | ||
| export default async function SettingsPage(props: OryPageParams) { | ||
| const flow = await getSettingsFlow(oryConfig, props.searchParams) | ||
|
|
||
| // getSettingsFlow has already redirected (created a flow / surfaced login). | ||
| if (!flow) { | ||
| return null | ||
| } | ||
|
|
||
| const [config, user] = await Promise.all([ | ||
| getOryConfigForRequest(), | ||
| getUserProfile(), | ||
| ]) | ||
|
|
||
| return ( | ||
| <SettingsCards | ||
| flow={flow} | ||
| config={config} | ||
| name={user?.name ?? null} | ||
| email={user?.email ?? null} | ||
| /> | ||
| ) | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.