Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
### Ory Network configuration (required when AUTH_PROVIDER=ory)
### SDK URL of the Ory Network project (or custom domain like https://auth.e2b.dev)
# ORY_SDK_URL=https://your-project.projects.oryapis.com
### Browser-facing Kratos public URL for the custom @ory/elements-react login
### page. Self-hosted: the Kratos public endpoint (harness default :4433).
### Ory Network: same value as ORY_SDK_URL.
# NEXT_PUBLIC_ORY_SDK_URL=http://localhost:4433
### Set to 1 to enable the custom @ory/elements-react login/registration UI
### (staging/preview and local dev). Leave unset in production to keep /sign-in.
# NEXT_PUBLIC_ORY_CUSTOM_UI=1
### OAuth2 client credentials issued by Ory for this dashboard deployment
# ORY_OAUTH2_CLIENT_ID=
# ORY_OAUTH2_CLIENT_SECRET=
Expand Down
112 changes: 111 additions & 1 deletion bun.lock

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion components.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,8 @@
"utils": "@/lib/utils",
"hooks": "@/lib/hooks"
},
"iconLibrary": "lucide"
"iconLibrary": "lucide",
"registries": {
"@svgl": "https://svgl.app/r/{name}.json"
}
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@
"@opentelemetry/sdk-node": "^0.218.0",
"@opentelemetry/semantic-conventions": "^1.36.0",
"@ory/client-fetch": "^1.22.37",
"@ory/elements-react": "^1.2.0",
"@ory/nextjs": "^1.0.0-rc.1",
"@radix-ui/react-avatar": "^1.1.4",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-dialog": "^1.1.15",
Expand Down
23 changes: 23 additions & 0 deletions src/app/login/components/custom-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use client'

import { FlowType } from '@ory/client-fetch'
import type { OryNodeButtonProps } from '@ory/elements-react'
import { useOryFlow } from '@ory/elements-react'
import { Button } from '@/ui/primitives/button'

export function OryButton({
node,
buttonProps,
isSubmitting,
}: OryNodeButtonProps) {
const { flowType } = useOryFlow()
const label = node.meta?.label?.text
const loadingLabel =
flowType === FlowType.Registration ? 'Signing up…' : 'Signing in…'

return (
<Button {...buttonProps} loading={isSubmitting ? loadingLabel : undefined}>
{label}
</Button>
)
}
11 changes: 11 additions & 0 deletions src/app/login/components/custom-card-header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
'use client'

import { FlowType } from '@ory/client-fetch'
import { useOryFlow } from '@ory/elements-react'

export function OryCardHeader() {
const { flowType } = useOryFlow()
const title = flowType === FlowType.Registration ? 'Sign up' : 'Sign in'

return <h1 className="mb-6">{title}</h1>
}
61 changes: 61 additions & 0 deletions src/app/login/components/custom-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
'use client'

import { FlowType } from '@ory/client-fetch'
import { useOryFlow } from '@ory/elements-react'
import Link from 'next/link'
import type { PropsWithChildren } from 'react'

export function OryCard({ children }: PropsWithChildren) {
return <div className="bg-bg flex w-full flex-col border p-6">{children}</div>
}

export function OryCardFooter() {
const { flowType } = useOryFlow()

if (flowType === FlowType.Login) {
return (
<p className="text-fg-secondary mt-6">
Don't have an account?{' '}
<Link href="/registration" className="text-fg underline">
Sign up
</Link>
.
</p>
)
}

if (flowType !== FlowType.Registration) {
return null
}

return (
<div className="text-fg-secondary mt-6 flex flex-col gap-4">
<p>
Already have an account?{' '}
<Link href="/login" className="text-fg underline">
Sign in
</Link>
.
</p>
<p className="text-fg-tertiary">
By signing up, you agree to our{' '}
<Link
href="/terms"
target="_blank"
className="text-fg-secondary underline"
>
Terms of Service
</Link>{' '}
and{' '}
<Link
href="/privacy"
target="_blank"
className="text-fg-secondary underline"
>
Privacy Policy
</Link>
.
</p>
</div>
)
}
7 changes: 7 additions & 0 deletions src/app/login/components/custom-divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import TextSeparator from '@/ui/text-separator'

export function OryDivider() {
return <TextSeparator text="or" />
}
7 changes: 7 additions & 0 deletions src/app/login/components/custom-form-group.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
'use client'

import type { PropsWithChildren } from 'react'

export function OryFormGroup({ children }: PropsWithChildren) {
return <div className="flex flex-col gap-4">{children}</div>
}
15 changes: 15 additions & 0 deletions src/app/login/components/custom-input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client'

import type { OryNodeInputProps } from '@ory/elements-react'
import { Input } from '@/ui/primitives/input'

export function OryInput({ inputProps, node }: OryNodeInputProps) {
const placeholder =
node.attributes.name === 'identifier' || inputProps.type === 'email'
? 'you@example.com'
: inputProps.type === 'password'
? '••••••••••••'
: undefined

return <Input {...inputProps} {...(placeholder ? { placeholder } : {})} />
}
39 changes: 39 additions & 0 deletions src/app/login/components/custom-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
'use client'

import type { OryNodeLabelProps } from '@ory/elements-react'
import { cn } from '@/lib/utils'
import { Label } from '@/ui/primitives/label'

export function OryLabel({ node, children, fieldError }: OryNodeLabelProps) {
const label = node.meta?.label?.text
const messages = node.messages ?? []
const fieldErrorText =
fieldError && typeof fieldError === 'object' && 'text' in fieldError
? String((fieldError as { text: unknown }).text)
: undefined

return (
<div className="flex w-full flex-col gap-2">
{label && <Label htmlFor={node.attributes.name}>{label}</Label>}
{children}
{messages.map((message) => (
<span
key={message.id}
className={cn(
'prose-label',
message.type === 'error'
? 'text-accent-error-highlight'
: 'text-fg-tertiary'
)}
>
{message.text}
</span>
))}
{fieldErrorText && (
<span className="prose-label text-accent-error-highlight">
{fieldErrorText}
</span>
)}
</div>
)
}
24 changes: 24 additions & 0 deletions src/app/login/components/custom-message.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import type { OryMessageContentProps } from '@ory/elements-react'
import type { PropsWithChildren } from 'react'
import { cn } from '@/lib/utils'

export function OryMessageRoot({ children }: PropsWithChildren) {
return <div className="flex flex-col gap-1">{children}</div>
}

export function OryMessageContent({ message }: OryMessageContentProps) {
return (
<span
className={cn(
'prose-label',
message.type === 'error'
? 'text-accent-error-highlight'
: 'text-fg-tertiary'
)}
>
{message.text}
</span>
)
}
57 changes: 57 additions & 0 deletions src/app/login/components/custom-sso-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use client'

import type { OryNodeSsoButtonProps } from '@ory/elements-react'
import type { ComponentType, SVGProps } from 'react'
import { Google } from '@/components/ui/svgs/google'
import { Button } from '@/ui/primitives/button'

const PROVIDERS: Record<
string,
{ label: string; Logo: ComponentType<SVGProps<SVGSVGElement>> }
> = {
google: { label: 'Continue with Google', Logo: Google },
}

// Ory's `provider` prop is the label *context* (a display name like "Google"),
// not a stable id — the id is on `node.attributes.value`. Match leniently across
// id, prop, and label text so casing / naming can't cause a miss.
function resolveProvider({
node,
provider,
}: Pick<OryNodeSsoButtonProps, 'node' | 'provider'>) {
const haystack = [
typeof node.attributes.value === 'string' ? node.attributes.value : '',
provider,
node.meta?.label?.text ?? '',
]
.join(' ')
.toLowerCase()

if (haystack.includes('google')) return PROVIDERS.google
return undefined
}

export function OrySsoButton({
node,
provider,
buttonProps,
isSubmitting,
}: OryNodeSsoButtonProps) {
const known = resolveProvider({ node, provider })
const label = known?.label ?? node.meta?.label?.text
const Logo = known?.Logo

return (
<Button
variant="secondary"
className="flex items-center gap-2"
{...buttonProps}
loading={isSubmitting ? 'Redirecting…' : undefined}
>
{Logo && (
<Logo className="h-5 w-5" aria-hidden="true" focusable="false" />
)}
{label}
</Button>
)
}
32 changes: 32 additions & 0 deletions src/app/login/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { OryFlowComponentOverrides } from '@ory/elements-react'
import { OryButton } from './custom-button'
import { OryCard, OryCardFooter } from './custom-card'
import { OryCardHeader } from './custom-card-header'
import { OryDivider } from './custom-divider'
import { OryFormGroup } from './custom-form-group'
import { OryInput } from './custom-input'
import { OryLabel } from './custom-label'
import { OryMessageContent, OryMessageRoot } from './custom-message'
import { OrySsoButton } from './custom-sso-button'

export const oryComponents: OryFlowComponentOverrides = {
Node: {
Button: OryButton,
SsoButton: OrySsoButton,
Input: OryInput,
Label: OryLabel,
},
Card: {
Root: OryCard,
Header: OryCardHeader,
Footer: OryCardFooter,
Divider: OryDivider,
},
Form: {
Group: OryFormGroup,
},
Message: {
Root: OryMessageRoot,
Content: OryMessageContent,
},
}
50 changes: 50 additions & 0 deletions src/app/login/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ALLOW_SEO_INDEXING } from '@/configs/flags'
import { METADATA } from '@/configs/metadata'
import { cn } from '@/lib/utils'
import { GridPattern } from '@/ui/grid-pattern'

// Dedicated layout for the custom Ory login page. Mirrors the (auth) group's
// background/centering, but omits its inner card wrapper: @ory/elements-react's
// <Login> renders its own self-contained card, so wrapping it again would
// double the border and overflow the narrow container.
//
// This route lives outside the (auth) group on purpose. A literal `auth/`
// folder cannot coexist with the (auth) group's internal `auth/` segment (used
// by /auth/cli) — Next.js rejects it as two parallel pages on the same path —
// so the custom login UI is served at the top-level /login instead.
export const metadata = {
title: METADATA.title,
description: METADATA.description,
openGraph: METADATA.openGraph,
twitter: METADATA.twitter,
robots: ALLOW_SEO_INDEXING ? 'index, follow' : 'noindex, nofollow',
}

export default function OryLoginLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="relative flex min-h-svh flex-col">
<GridPattern
width={50}
height={50}
x={-1}
y={-1}
strokeDasharray={'4 2'}
className={cn(
'[mask-image:radial-gradient(800px_400px_at_center,white,transparent)]',
'z-10'
)}
gradientFrom="var(--accent-main-highlight )"
gradientVia="var(--bg-highlight)"
gradientTo="var(--fill-highlight)"
gradientDegrees={90}
/>
<div className="z-10 flex w-full flex-1 items-center justify-center px-4 py-4">
<div className="w-full max-w-96">{children}</div>
</div>
</div>
)
}
17 changes: 17 additions & 0 deletions src/app/login/login-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client'

import { Login } from '@ory/elements-react/theme'
import type { ComponentProps } from 'react'
import { oryComponents } from './components'

// Overrides are functions, so they're applied client-side here, not in the
// server page. Prop types come from <Login> itself: two @ory/client-fetch
// copies are installed, so naming the LoginFlow type directly would mismatch.
type LoginProps = ComponentProps<typeof Login>

export function LoginCard({
flow,
config,
}: Pick<LoginProps, 'flow' | 'config'>) {
return <Login flow={flow} config={config} components={oryComponents} />
}
Loading
Loading