Skip to content
Open
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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ storybook-static
pnpm-lock.yaml
/test-results/
.nx
coverage/
coverage/
4 changes: 2 additions & 2 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -182,8 +182,8 @@ const config = tseslint.config([
],
},
],
}
}
},
},
])

export default config
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { acceptConsentRequest, rejectConsentRequest } from "@ory/nextjs/app"
import { NextResponse } from "next/server"

interface ConsentBody {
action?: string
consent_challenge?: string
grant_scope?: string | string[]
remember?: boolean | string
}

async function parseRequest(request: Request): Promise<ConsentBody> {
const contentType = request.headers.get("content-type") || ""

if (contentType.includes("application/json")) {
return (await request.json()) as ConsentBody
}

if (
contentType.includes("application/x-www-form-urlencoded") ||
contentType.includes("multipart/form-data")
) {
const formData = await request.formData()
return {
action: formData.get("action") as string,
consent_challenge: formData.get("consent_challenge") as string,
grant_scope: formData.getAll("grant_scope") as string[],
remember: formData.get("remember") as string,
}
}

// Try JSON as fallback
try {
return (await request.json()) as ConsentBody
} catch {
return {}
}
}

export async function POST(request: Request) {
const body = await parseRequest(request)

const action = body.action
const consentChallenge = body.consent_challenge
const grantScope = Array.isArray(body.grant_scope)
? body.grant_scope
: body.grant_scope
? [body.grant_scope]
: []
const remember = body.remember === true || body.remember === "true"

if (!consentChallenge) {
return NextResponse.json(
{
error: "invalid_request",
error_description: "Missing consent_challenge",
},
{ status: 400 },
)
}

try {
let redirectTo: string

if (action === "accept") {
redirectTo = await acceptConsentRequest(consentChallenge, {
grantScope,
remember,
})
} else {
redirectTo = await rejectConsentRequest(consentChallenge)
}

return NextResponse.json({ redirect_to: redirectTo })
} catch (error) {
console.error("Consent error:", error)
return NextResponse.json(
{ error: "server_error", error_description: "Failed to process consent" },
{ status: 500 },
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { Consent } from "@ory/elements-react/theme"
import {
getConsentFlow,
getServerSession,
OryPageParams,
} from "@ory/nextjs/app"

import { myCustomComponents } from "@/components"
import config from "@/ory.config"

export default async function ConsentPage(props: OryPageParams) {
const consentRequest = await getConsentFlow(props.searchParams)
const session = await getServerSession()

if (!consentRequest || !session) {
return null
}

return (
<Consent
consentChallenge={consentRequest}
session={session}
config={config}
csrfToken=""
formActionUrl="/api/consent"
components={myCustomComponents}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { UiNode, UiNodeInputAttributesTypeEnum } from "@ory/client-fetch"
import { isUiNodeInput, UiNodeInput } from "@ory/elements-react"

/**
* Finds consent-specific nodes from the UI nodes list.
*/
export function findConsentNodes(nodes: UiNode[]) {
let rememberNode: UiNodeInput | undefined
const submitNodes: UiNodeInput[] = []

for (const node of nodes) {
if (!isUiNodeInput(node)) {
continue
}

if (node.attributes.name === "remember") {
rememberNode = node
} else if (node.attributes.type === UiNodeInputAttributesTypeEnum.Submit) {
submitNodes.push(node)
}
}

return { rememberNode, submitNodes }
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

"use client"
import { getNodeLabel } from "@ory/client-fetch"
import { OryNodeButtonProps } from "@ory/elements-react"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { getNodeLabel } from "@ory/client-fetch"
import { OryNodeCheckboxProps } from "@ory/elements-react"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright © 2024 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeConsentScopeCheckboxProps } from "@ory/elements-react"

const scopeLabels: Record<string, { title: string; description: string }> = {
openid: {
title: "Identity",
description: "Allows the application to verify your identity.",
},
offline_access: {
title: "Offline Access",
description: "Allows the application to keep you signed in.",
},
profile: {
title: "Profile Information",
description: "Allows access to your basic profile details.",
},
email: {
title: "Email Address",
description: "Retrieve your email address and its verification status.",
},
phone: {
title: "Phone Number",
description: "Retrieve your phone number.",
},
}

export function MyCustomConsentScopeCheckbox({
attributes,
onCheckedChange,
inputProps,
}: OryNodeConsentScopeCheckboxProps) {
const scope = attributes.value as string
const label = scopeLabels[scope] ?? { title: scope, description: "" }

return (
<label className="flex items-center justify-between p-3 border rounded-lg cursor-pointer hover:bg-gray-50">
<div className="flex-1">
<p className="font-medium text-gray-900">{label.title}</p>
{label.description && (
<p className="text-sm text-gray-500">{label.description}</p>
)}
</div>
<div className="ml-4">
<button
type="button"
role="switch"
aria-checked={inputProps.checked}
onClick={() => onCheckedChange(!inputProps.checked)}
disabled={inputProps.disabled}
className={`
relative inline-flex h-6 w-11 items-center rounded-full transition-colors
${inputProps.checked ? "bg-blue-600" : "bg-gray-300"}
${inputProps.disabled ? "opacity-50 cursor-not-allowed" : ""}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
${inputProps.checked ? "translate-x-6" : "translate-x-1"}
`}
/>
</button>
</div>
</label>
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

/* eslint-disable @typescript-eslint/no-unsafe-enum-comparison -- eslint gets confused because of different versions of @ory/client-fetch */
import { FlowType } from "@ory/client-fetch"
import { useOryFlow } from "@ory/elements-react"
import { ConsentFlow, Node, useOryFlow } from "@ory/elements-react"
import Link from "next/link"
import { findConsentNodes } from "./consent-utils"

export function MyCustomFooter() {
const flow = useOryFlow()
Expand Down Expand Up @@ -29,8 +33,40 @@ export function MyCustomFooter() {
case FlowType.Verification:
return null
case FlowType.OAuth2Consent:
return null
return <ConsentFooter flow={flow.flow} />
default:
return null
}
}

function ConsentFooter({ flow }: { flow: ConsentFlow }) {
const { rememberNode, submitNodes } = findConsentNodes(flow.ui.nodes)
const clientName =
flow.consent_request.client?.client_name ?? "this application"

return (
<div className="flex flex-col gap-4">
<div>
<p className="font-medium text-gray-700">
Make sure you trust {clientName}
</p>
<p className="text-sm text-gray-500">
You may be sharing sensitive information with this site or
application.
</p>
</div>

{rememberNode && <Node.Checkbox node={rememberNode} />}

<div className="grid grid-cols-2 gap-2">
{submitNodes.map((node) => (
<Node.Button key={String(node.attributes.value)} node={node} />
))}
</div>

<p className="text-xs text-gray-400">
Authorizing will redirect to {clientName}
</p>
</div>
)
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeImageProps } from "@ory/elements-react"

export function MyCustomImage({ node }: OryNodeImageProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeInputProps } from "@ory/elements-react"

export function MyCustomInput({ inputProps }: OryNodeInputProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeLabelProps } from "@ory/elements-react"

export function MyCustomLabel({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeInputProps } from "@ory/elements-react"

export function MyCustomPinCodeInput({ inputProps }: OryNodeInputProps) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

import { OryNodeSsoButtonProps } from "@ory/elements-react"
import { IconBrandGoogle, IconTopologyRing } from "@tabler/icons-react"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright © 2026 Ory Corp
// SPDX-License-Identifier: Apache-2.0

"use client"
import { OryFlowComponentOverrides } from "@ory/elements-react"
import { MyCustomButton } from "./custom-button"
Expand All @@ -6,6 +9,7 @@ import { MyCustomSsoButton } from "./custom-social"
import { MyCustomInput } from "./custom-input"
import { MyCustomPinCodeInput } from "./custom-pin-code"
import { MyCustomCheckbox } from "./custom-checkbox"
import { MyCustomConsentScopeCheckbox } from "./custom-consent-scope-checkbox"
import { MyCustomImage } from "./custom-image"
import { MyCustomLabel } from "./custom-label"
import { MyCustomFooter } from "./custom-footer"
Expand All @@ -17,6 +21,7 @@ export const myCustomComponents: OryFlowComponentOverrides = {
Input: MyCustomInput,
CodeInput: MyCustomPinCodeInput,
Checkbox: MyCustomCheckbox,
ConsentScopeCheckbox: MyCustomConsentScopeCheckbox,
Image: MyCustomImage,
Label: MyCustomLabel,
},
Expand Down
Loading
Loading