<@U04UQ68083H> create a page for login using ory c...
# ory-copilot
c
@chilly-king-10285 create a page for login using ory client in next js with passkey and otp with custom UI , give me complete code
b
A message from kapa.ai
I understand you're looking to create a custom login page in Next.js using Ory client with passkey and OTP support. Based on the knowledge sources provided, I can guide you on how to implement a custom UI for Ory authentication in Next.js. The knowledge sources don't contain complete code examples specifically for passkey and OTP with custom UI in Next.js. However, I can provide you with a foundation based on the available information about creating custom login flows with Ory in Next.js. Here's how you can create a basic login page with custom UI in Next.js using the Ory client: ```tsx // pages/login.tsx import { Configuration, FrontendApi, LoginFlow, UiNode, UiNodeInputAttributes, } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ basePath:
https://${process.env.NEXT_PUBLIC_ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>
, baseOptions: { withCredentials: true, // we need to include cookies }, }), )
export default function Login() { const [flow, setFlow] = useState<LoginFlow>() const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow as string if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to as string frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) as any // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = ( event.nativeEvent as unknown as { submitter: HTMLInputElement } ).submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to as string || "/") }) .catch((err) => { if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err) } }) } const renderNode = (node: UiNode) => { if (isUiNodeInputAttributes(node.attributes)) { const attrs = node.attributes as UiNodeInputAttributes switch (attrs.type) { case "button": case "submit": return ( <button key={attrs.name} type={attrs.type as "submit" | "reset" | "button" | undefined} name={attrs.name} value={attrs.value} className="px-4 py-2 bg-blue-500 text-white rounded" > {node.meta.label?.text || attrs.value} </button> ) default: return (
<div key={attrs.name} className="mb-4"> <label className="block text-gray-700 mb-2"> {node.meta.label?.text} </label> <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } } return null } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Render password method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "password"], }).map(renderNode)} </div> {/* Render webauthn/passkey method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "webauthn"], }).map(renderNode)} </div> {/* Render OTP method fields */} <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">OTP Login</h2> {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default", "totp"], }).map(renderNode)} </div> </form> </div> ) } ```
I understand you're looking to create a custom login page in Next.js using Ory client with passkey and OTP support. Based on the knowledge sources provided, I can guide you on how to implement a custom UI for Ory authentication in Next.js. The knowledge sources don't contain complete code examples specifically for passkey and OTP with custom UI in Next.js. However, I can provide you with a foundation based on the available information about creating custom login flows with Ory in Next.js. Here's how you can create a basic login page with custom UI in Next.js using the Ory client:
Copy code
tsx // pages/login.tsx import {   Configuration,   FrontendApi,   LoginFlow,   UiNode,   UiNodeInputAttributes, } from "@ory/client" import {   filterNodesByGroups,   isUiNodeInputAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next"  const frontend = new FrontendApi(   new Configuration({     basePath: `https://${process.env.NEXT_PUBLIC_ORY_PROJECT_SLUG}.<http://projects.oryapis.com|projects.oryapis.com>`,     baseOptions: {       withCredentials: true, // we need to include cookies     },   }), )  export default function Login() {   const [flow, setFlow] = useState<LoginFlow>()   const router = useRouter()      useEffect(() => {     // Check if flow ID exists in URL     const flowId = router.query.flow as string          if (flowId) {       // If we have a flow ID, get the flow       frontend         .getLoginFlow({ id: flowId })         .then(({ data }) => {           setFlow(data)         })         .catch((err) => {           console.error("Failed to get login flow", err)           // Start a new flow if there was an error           initializeFlow()         })     } else {       // Initialize a new flow       initializeFlow()     }   }, [router.query.flow])      const initializeFlow = () => {     // Get the return_to from the URL     const returnTo = router.query.return_to as string          frontend       .createBrowserLoginFlow({         returnTo: returnTo || "/", // redirect to the root path after login         refresh: true, // if the user has a session, refresh it       })       .then(({ data }) => {         setFlow(data)       })       .catch((err) => {         console.error("Failed to initialize login flow", err)       })   }      const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {     event.preventDefault()          if (!flow) return          const form = event.currentTarget     const formData = new FormData(form)          // Map the entire form data to JSON for the request body     let body = Object.fromEntries(formData) as any          // We need the method specified from the name and value of the submit button     if ("submitter" in event.nativeEvent) {       const method = (         event.nativeEvent as unknown as { submitter: HTMLInputElement }       ).submitter       body = {         ...body,         ...{ [method.name]: method.value },       }     }          frontend       .updateLoginFlow({         flow: flow.id,         updateLoginFlowBody: body,       })       .then(() => {         // Redirect to home page or the return_to URL         router.push(router.query.return_to as string || "/")       })       .catch((err) => {         if (err.response?.status === 400) {           // User input error, update the flow with error messages           setFlow(err.response.data)         } else {           console.error("Login failed", err)         }       })   }      const renderNode = (node: UiNode) => {     if (isUiNodeInputAttributes(node.attributes)) {       const attrs = node.attributes as UiNodeInputAttributes              switch (attrs.type) {         case "button":         case "submit":           return (             <button               key={attrs.name}               type={attrs.type as "submit" | "reset" | "button" | undefined}               name={attrs.name}               value={attrs.value}               className="px-4 py-2 bg-blue-500 text-white rounded"             >               {node.meta.label?.text || attrs.value}             </button>           )         default:           return (             <div key={attrs.name} className="mb-4">               <label className="block text-gray-700 mb-2">                 {node.meta.label?.text}               </label>               <input                 name={attrs.name}                 type={attrs.type}                 autoComplete={attrs.autocomplete}                 defaultValue={attrs.value}                 required={attrs.required}                 disabled={attrs.disabled}                 className="w-full px-3 py-2 border rounded"               />               {node.messages?.map((message, idx) => (                 <p key={idx} className="text-red-500 text-sm mt-1">                   {message.text}                 </p>               ))}             </div>           )       }     }     return null   }      if (!flow) {     return <div>Loading...</div>   }      return (     <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md">       <h1 className="text-2xl font-bold mb-6">Login</h1>              <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}>         {/* Display any flow level messages/errors */}         {flow.ui.messages?.map((message, idx) => (           <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded">             {message.text}           </div>         ))}                  {/* Render password method fields */}         <div className="mb-6">           <h2 className="text-lg font-semibold mb-4">Password Login</h2>           {filterNodesByGroups({             nodes: flow.ui.nodes,             groups: ["default", "password"],           }).map(renderNode)}         </div>                  {/* Render webauthn/passkey method fields */}         <div className="mb-6">           <h2 className="text-lg font-semibold mb-4">Passkey Login</h2>           {filterNodesByGroups({             nodes: flow.ui.nodes,             groups: ["default", "webauthn"],           }).map(renderNode)}         </div>                  {/* Render OTP method fields */}         <div className="mb-6">           <h2 className="text-lg font-semibold mb-4">OTP Login</h2>           {filterNodesByGroups({             nodes: flow.ui.nodes,             groups: ["default", "totp"],           }).map(renderNode)}         </div>       </form>     </div>   ) }
This is a basic implementation that provides a foundation for your login page. To make this work properly, you'll need to: 1. Set up an Ory Network project as mentioned in the [prerequisites for custom UI](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements#prerequisites). 2. Install the required dependencies:
Copy code
npm install @ory/client @ory/integrations
3. Configure your Next.js application to connect to your Ory Network project by setting the
NEXT_PUBLIC_ORY_PROJECT_SLUG
environment variable. For a more complete implementation with Ory Elements (which provides pre-built UI components), you can follow the guide on [Custom user interface with Ory Elements](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements). To run the application locally and connect it to your Ory Network project, you'll need to use Ory Tunnel as described in the [Run the sample application](https://www.ory.sh/docs/kratos/bring-your-own-ui/custom-ui-ory-elements#run-the-sample-application) section. Note that the actual UI for passkeys and OTP will depend on your Ory Network project configuration. You'll need to enable these authentication methods in your Ory Network project settings for them to appear in the login flow.
c
@chilly-king-10285 i'm not able to see the submit buttons , can you update the code ?
b
A message from kapa.ai
I understand you're having trouble with the submit buttons not appearing in the login form. This is a common issue when working with Ory's UI nodes. Let me update the code to ensure the submit buttons are properly rendered. Based on the issue described in the knowledge sources about
onSubmit
not being called with passwordless one-time codes, I'll make some adjustments to improve the button rendering: ```tsx // pages/login.tsx import { Configuration, FrontendApi, LoginFlow, UiNode, UiNodeInputAttributes, UiNodeScriptAttributes, } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, isUiNodeScriptAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ ...edgeConfig, baseOptions: { withCredentials: true, // we need to include cookies }, }), )
export default function Login() { const [flow, setFlow] = useState<LoginFlow>() const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow as string if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) // Add WebAuthn scripts if present useEffect(() => { if (!flow) return const scriptNodes = flow.ui.nodes.filter(node => isUiNodeScriptAttributes(node.attributes) ) scriptNodes.forEach(node => { const attrs = node.attributes as UiNodeScriptAttributes const script = document.createElement('script') script.src = attrs.src script.async = attrs.async script.id =
ory-script-${attrs.src}
// Only add if not already present if (!document.getElementById(script.id)) { document.body.appendChild(script) } }) return () => { scriptNodes.forEach(node => { const script = document.getElementById(
ory-script-${(node.attributes as UiNodeScriptAttributes).src}
) if (script) { document.body.removeChild(script) } }) } }, [flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to as string frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) as any // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = ( event.nativeEvent as unknown as { submitter: HTMLInputElement } ).submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to as string || "/") }) .catch((err) => {
if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err) } }) } // Specific function to render submit buttons correctly const renderSubmitButton = (node: UiNode) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes as UiNodeInputAttributes if (attrs.type !== 'submit') return null return ( <button key={
${attrs.name}-${attrs.value}
} type="submit" name={attrs.name} value={attrs.value} className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2" onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined} > {node.meta?.label?.text || attrs.value} </button> ) } const renderInputField = (node: UiNode) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes as UiNodeInputAttributes if (attrs.type === 'submit' || attrs.type === 'button') return null return ( <div key={attrs.name} className="mb-4"> {node.meta?.label?.text && ( <label className="block text-gray-700 mb-2"> {node.meta.label.text} </label> )} <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Hidden fields (like CSRF token) */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default"], attributes: ["hidden"], }).map(renderInputField)} {/* Password method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {/* Password input fields */}
{filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Password submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* Webauthn/Passkey method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {/* Webauthn input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Webauthn submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* OTP method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2> {/* OTP input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* OTP submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} </form> </div> ) } ```
I understand you're having trouble with the submit buttons not appearing in the login form. This is a common issue when working with Ory's UI nodes. Let me update the code to ensure the submit buttons are properly rendered. Based on the issue described in the knowledge sources about
onSubmit
not being called with passwordless one-time codes, I'll make some adjustments to improve the button rendering:
Copy code
tsx // pages/login.tsx import {   Configuration,   FrontendApi,   LoginFlow,   UiNode,   UiNodeInputAttributes,   UiNodeScriptAttributes, } from "@ory/client" import {   filterNodesByGroups,   isUiNodeInputAttributes,   isUiNodeScriptAttributes, } from "@ory/integrations/ui" import { useEffect, useState } from "react" import { useRouter } from "next/router" import { edgeConfig } from "@ory/integrations/next"  const frontend = new FrontendApi(   new Configuration({     ...edgeConfig,     baseOptions: {       withCredentials: true, // we need to include cookies     },   }), )  export default function Login() {   const [flow, setFlow] = useState<LoginFlow>()   const router = useRouter()      useEffect(() => {     // Check if flow ID exists in URL     const flowId = router.query.flow as string          if (flowId) {       // If we have a flow ID, get the flow       frontend         .getLoginFlow({ id: flowId })         .then(({ data }) => {           setFlow(data)         })         .catch((err) => {           console.error("Failed to get login flow", err)           // Start a new flow if there was an error           initializeFlow()         })     } else {       // Initialize a new flow       initializeFlow()     }   }, [router.query.flow])      // Add WebAuthn scripts if present   useEffect(() => {     if (!flow) return          const scriptNodes = flow.ui.nodes.filter(node =>        isUiNodeScriptAttributes(node.attributes)     )          scriptNodes.forEach(node => {       const attrs = node.attributes as UiNodeScriptAttributes       const script = document.createElement('script')       script.src = attrs.src       script.async = attrs.async       script.id = `ory-script-${attrs.src}`              // Only add if not already present       if (!document.getElementById(script.id)) {         document.body.appendChild(script)       }     })          return () => {       scriptNodes.forEach(node => {         const script = document.getElementById(`ory-script-${(node.attributes as UiNodeScriptAttributes).src}`)         if (script) {           document.body.removeChild(script)         }       })     }   }, [flow])      const initializeFlow = () => {     // Get the return_to from the URL     const returnTo = router.query.return_to as string          frontend       .createBrowserLoginFlow({         returnTo: returnTo || "/", // redirect to the root path after login         refresh: true, // if the user has a session, refresh it       })       .then(({ data }) => {         setFlow(data)       })       .catch((err) => {         console.error("Failed to initialize login flow", err)       })   }      const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {     event.preventDefault()          if (!flow) return          const form = event.currentTarget     const formData = new FormData(form)          // Map the entire form data to JSON for the request body     let body = Object.fromEntries(formData) as any          // We need the method specified from the name and value of the submit button     if ("submitter" in event.nativeEvent) {       const method = (         event.nativeEvent as unknown as { submitter: HTMLInputElement }       ).submitter       body = {         ...body,         ...{ [method.name]: method.value },       }     }          frontend       .updateLoginFlow({         flow: flow.id,         updateLoginFlowBody: body,       })       .then(() => {         // Redirect to home page or the return_to URL         router.push(router.query.return_to as string || "/")       })       .catch((err) => {         if (err.response?.status === 400) {           // User input error, update the flow with error messages           setFlow(err.response.data)         } else {           console.error("Login failed", err)         }       })   }      // Specific function to render submit buttons correctly   const renderSubmitButton = (node: UiNode) => {     if (!isUiNodeInputAttributes(node.attributes)) return null          const attrs = node.attributes as UiNodeInputAttributes     if (attrs.type !== 'submit') return null          return (       <button         key={`${attrs.name}-${attrs.value}`}         type="submit"         name={attrs.name}         value={attrs.value}         className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2"         onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined}       >         {node.meta?.label?.text || attrs.value}       </button>     )   }      const renderInputField = (node: UiNode) => {     if (!isUiNodeInputAttributes(node.attributes)) return null          const attrs = node.attributes as UiNodeInputAttributes     if (attrs.type === 'submit' || attrs.type === 'button') return null          return (       <div key={attrs.name} className="mb-4">         {node.meta?.label?.text && (           <label className="block text-gray-700 mb-2">             {node.meta.label.text}           </label>         )}         <input           name={attrs.name}           type={attrs.type}           autoComplete={attrs.autocomplete}           defaultValue={attrs.value}           required={attrs.required}           disabled={attrs.disabled}           className="w-full px-3 py-2 border rounded"         />         {node.messages?.map((message, idx) => (           <p key={idx} className="text-red-500 text-sm mt-1">             {message.text}           </p>         ))}       </div>     )   }      if (!flow) {     return <div>Loading...</div>   }      return (     <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md">       <h1 className="text-2xl font-bold mb-6">Login</h1>              <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}>         {/* Display any flow level messages/errors */}         {flow.ui.messages?.map((message, idx) => (           <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded">             {message.text}           </div>         ))}                  {/* Hidden fields (like CSRF token) */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["default"],           attributes: ["hidden"],         }).map(renderInputField)}                  {/* Password method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["password"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">Password Login</h2>             {/* Password input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["password"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* Password submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["password"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}                  {/* Webauthn/Passkey method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["webauthn"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">Passkey Login</h2>             {/* Webauthn input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["webauthn"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* Webauthn submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["webauthn"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}                  {/* OTP method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["totp", "code"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2>             {/* OTP input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["totp", "code"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* OTP submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["totp", "code"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}       </form>     </div>   ) }
The key improvements in this updated code: 1. I've separated the rendering of input fields and submit buttons into distinct functions to ensure buttons are properly displayed. 2. I've added specific handling for WebAuthn scripts that are required for passkey functionality. 3. I've improved the filtering of UI nodes to explicitly look for submit buttons with the
attributes: ["submit"]
filter. 4. I've added conditional rendering for each authentication method section, so they only appear if relevant nodes are present. 5. For the passwordless one-time codes, I've included both "totp" and "code" groups to ensure all OTP-related nodes are captured. 6. I've added proper handling for the
onclick
attribute that's often needed for WebAuthn/passkey buttons to work correctly. This should resolve the issue with submit buttons not appearing in your login form. If you're still experiencing issues, make sure your Ory project has the relevant authentication methods (password, passkey, OTP) enabled in your configuration.
c
@chilly-king-10285 please give me code in javascript only
b
A message from kapa.ai
I understand you'd like the login page code in JavaScript instead of TypeScript. Here's the same implementation using JavaScript for Next.js: ```jsx // pages/login.jsx import { useEffect, useState } from "react" import { useRouter } from "next/router" import { Configuration, FrontendApi } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, isUiNodeScriptAttributes } from "@ory/integrations/ui" import { edgeConfig } from "@ory/integrations/next" const frontend = new FrontendApi( new Configuration({ ...edgeConfig, baseOptions: { withCredentials: true, // we need to include cookies }, }), )
export default function Login() { const [flow, setFlow] = useState(null) const router = useRouter() useEffect(() => { // Check if flow ID exists in URL const flowId = router.query.flow if (flowId) { // If we have a flow ID, get the flow frontend .getLoginFlow({ id: flowId }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to get login flow", err) // Start a new flow if there was an error initializeFlow() }) } else { // Initialize a new flow initializeFlow() } }, [router.query.flow]) // Add WebAuthn scripts if present useEffect(() => { if (!flow) return const scriptNodes = flow.ui.nodes.filter(node => isUiNodeScriptAttributes(node.attributes) ) scriptNodes.forEach(node => { const attrs = node.attributes const script = document.createElement('script') script.src = attrs.src script.async = attrs.async script.id =
ory-script-${attrs.src}
// Only add if not already present if (!document.getElementById(script.id)) { document.body.appendChild(script) } }) return () => { scriptNodes.forEach(node => { const script = document.getElementById(
ory-script-${node.attributes.src}
) if (script) { document.body.removeChild(script) } }) } }, [flow]) const initializeFlow = () => { // Get the return_to from the URL const returnTo = router.query.return_to frontend .createBrowserLoginFlow({ returnTo: returnTo || "/", // redirect to the root path after login refresh: true, // if the user has a session, refresh it }) .then(({ data }) => { setFlow(data) }) .catch((err) => { console.error("Failed to initialize login flow", err) }) } const handleSubmit = (event) => { event.preventDefault() if (!flow) return const form = event.currentTarget const formData = new FormData(form) // Map the entire form data to JSON for the request body let body = Object.fromEntries(formData) // We need the method specified from the name and value of the submit button if ("submitter" in event.nativeEvent) { const method = event.nativeEvent.submitter body = { ...body, ...{ [method.name]: method.value }, } } frontend .updateLoginFlow({ flow: flow.id, updateLoginFlowBody: body, }) .then(() => { // Redirect to home page or the return_to URL router.push(router.query.return_to || "/") }) .catch((err) => { if (err.response?.status === 400) { // User input error, update the flow with error messages setFlow(err.response.data) } else { console.error("Login failed", err)
} }) } // Specific function to render submit buttons correctly const renderSubmitButton = (node) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes if (attrs.type !== 'submit') return null return ( <button key={
${attrs.name}-${attrs.value}
} type="submit" name={attrs.name} value={attrs.value} className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2" onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined} > {node.meta?.label?.text || attrs.value} </button> ) } const renderInputField = (node) => { if (!isUiNodeInputAttributes(node.attributes)) return null const attrs = node.attributes if (attrs.type === 'submit' || attrs.type === 'button') return null return ( <div key={attrs.name} className="mb-4"> {node.meta?.label?.text && ( <label className="block text-gray-700 mb-2"> {node.meta.label.text} </label> )} <input name={attrs.name} type={attrs.type} autoComplete={attrs.autocomplete} defaultValue={attrs.value} required={attrs.required} disabled={attrs.disabled} className="w-full px-3 py-2 border rounded" /> {node.messages?.map((message, idx) => ( <p key={idx} className="text-red-500 text-sm mt-1"> {message.text} </p> ))} </div> ) } if (!flow) { return <div>Loading...</div> } return ( <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md"> <h1 className="text-2xl font-bold mb-6">Login</h1> <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}> {/* Display any flow level messages/errors */} {flow.ui.messages?.map((message, idx) => ( <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded"> {message.text} </div> ))} {/* Hidden fields (like CSRF token) */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["default"], attributes: ["hidden"], }).map(renderInputField)} {/* Password method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Password Login</h2> {/* Password input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["password"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Password submit button */} {filterNodesByGroups({
nodes: flow.ui.nodes, groups: ["password"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* Webauthn/Passkey method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">Passkey Login</h2> {/* Webauthn input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* Webauthn submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["webauthn"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} {/* OTP method */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden"], }).length > 0 && ( <div className="mb-6"> <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2> {/* OTP input fields */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], withoutAttributes: ["hidden", "submit"], }).map(renderInputField)} {/* OTP submit button */} {filterNodesByGroups({ nodes: flow.ui.nodes, groups: ["totp", "code"], attributes: ["submit"], }).map(renderSubmitButton)} </div> )} </form> </div> ) } ```
I understand you'd like the login page code in JavaScript instead of TypeScript. Here's the same implementation using JavaScript for Next.js:
Copy code
jsx // pages/login.jsx import { useEffect, useState } from "react" import { useRouter } from "next/router" import { Configuration, FrontendApi } from "@ory/client" import { filterNodesByGroups, isUiNodeInputAttributes, isUiNodeScriptAttributes } from "@ory/integrations/ui" import { edgeConfig } from "@ory/integrations/next"  const frontend = new FrontendApi(   new Configuration({     ...edgeConfig,     baseOptions: {       withCredentials: true, // we need to include cookies     },   }), )  export default function Login() {   const [flow, setFlow] = useState(null)   const router = useRouter()      useEffect(() => {     // Check if flow ID exists in URL     const flowId = router.query.flow          if (flowId) {       // If we have a flow ID, get the flow       frontend         .getLoginFlow({ id: flowId })         .then(({ data }) => {           setFlow(data)         })         .catch((err) => {           console.error("Failed to get login flow", err)           // Start a new flow if there was an error           initializeFlow()         })     } else {       // Initialize a new flow       initializeFlow()     }   }, [router.query.flow])      // Add WebAuthn scripts if present   useEffect(() => {     if (!flow) return          const scriptNodes = flow.ui.nodes.filter(node =>        isUiNodeScriptAttributes(node.attributes)     )          scriptNodes.forEach(node => {       const attrs = node.attributes       const script = document.createElement('script')       script.src = attrs.src       script.async = attrs.async       script.id = `ory-script-${attrs.src}`              // Only add if not already present       if (!document.getElementById(script.id)) {         document.body.appendChild(script)       }     })          return () => {       scriptNodes.forEach(node => {         const script = document.getElementById(`ory-script-${node.attributes.src}`)         if (script) {           document.body.removeChild(script)         }       })     }   }, [flow])      const initializeFlow = () => {     // Get the return_to from the URL     const returnTo = router.query.return_to          frontend       .createBrowserLoginFlow({         returnTo: returnTo || "/", // redirect to the root path after login         refresh: true, // if the user has a session, refresh it       })       .then(({ data }) => {         setFlow(data)       })       .catch((err) => {         console.error("Failed to initialize login flow", err)       })   }      const handleSubmit = (event) => {     event.preventDefault()          if (!flow) return          const form = event.currentTarget     const formData = new FormData(form)          // Map the entire form data to JSON for the request body     let body = Object.fromEntries(formData)          // We need the method specified from the name and value of the submit button     if ("submitter" in event.nativeEvent) {       const method = event.nativeEvent.submitter       body = {         ...body,         ...{ [method.name]: method.value },       }     }          frontend       .updateLoginFlow({         flow: flow.id,         updateLoginFlowBody: body,       })       .then(() => {         // Redirect to home page or the return_to URL         router.push(router.query.return_to || "/")       })       .catch((err) => {         if (err.response?.status === 400) {           // User input error, update the flow with error messages           setFlow(err.response.data)         } else {           console.error("Login failed", err)         }       })   }      // Specific function to render submit buttons correctly   const renderSubmitButton = (node) => {     if (!isUiNodeInputAttributes(node.attributes)) return null          const attrs = node.attributes     if (attrs.type !== 'submit') return null          return (       <button         key={`${attrs.name}-${attrs.value}`}         type="submit"         name={attrs.name}         value={attrs.value}         className="w-full px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 mb-2"         onClick={attrs.onclick ? () => { eval(attrs.onclick) } : undefined}       >         {node.meta?.label?.text || attrs.value}       </button>     )   }      const renderInputField = (node) => {     if (!isUiNodeInputAttributes(node.attributes)) return null          const attrs = node.attributes     if (attrs.type === 'submit' || attrs.type === 'button') return null          return (       <div key={attrs.name} className="mb-4">         {node.meta?.label?.text && (           <label className="block text-gray-700 mb-2">             {node.meta.label.text}           </label>         )}         <input           name={attrs.name}           type={attrs.type}           autoComplete={attrs.autocomplete}           defaultValue={attrs.value}           required={attrs.required}           disabled={attrs.disabled}           className="w-full px-3 py-2 border rounded"         />         {node.messages?.map((message, idx) => (           <p key={idx} className="text-red-500 text-sm mt-1">             {message.text}           </p>         ))}       </div>     )   }      if (!flow) {     return <div>Loading...</div>   }      return (     <div className="max-w-md mx-auto mt-10 p-6 bg-white rounded shadow-md">       <h1 className="text-2xl font-bold mb-6">Login</h1>              <form action={flow.ui.action} method={flow.ui.method} onSubmit={handleSubmit}>         {/* Display any flow level messages/errors */}         {flow.ui.messages?.map((message, idx) => (           <div key={idx} className="mb-4 p-2 bg-red-100 text-red-700 rounded">             {message.text}           </div>         ))}                  {/* Hidden fields (like CSRF token) */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["default"],           attributes: ["hidden"],         }).map(renderInputField)}                  {/* Password method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["password"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">Password Login</h2>             {/* Password input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["password"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* Password submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["password"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}                  {/* Webauthn/Passkey method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["webauthn"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">Passkey Login</h2>             {/* Webauthn input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["webauthn"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* Webauthn submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["webauthn"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}                  {/* OTP method */}         {filterNodesByGroups({           nodes: flow.ui.nodes,           groups: ["totp", "code"],           withoutAttributes: ["hidden"],         }).length > 0 && (           <div className="mb-6">             <h2 className="text-lg font-semibold mb-4">One-Time Code Login</h2>             {/* OTP input fields */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["totp", "code"],               withoutAttributes: ["hidden", "submit"],             }).map(renderInputField)}                          {/* OTP submit button */}             {filterNodesByGroups({               nodes: flow.ui.nodes,               groups: ["totp", "code"],               attributes: ["submit"],             }).map(renderSubmitButton)}           </div>         )}       </form>     </div>   ) }
This JavaScript version maintains all the functionality of the TypeScript version but removes the type annotations. The code handles password, passkey (WebAuthn), and OTP authentication methods, and properly renders the submit buttons for each method. To use this code, make sure you've installed the required dependencies:
Copy code
npm install @ory/client @ory/integrations
And set up the Ory API connection in your Next.js application as described in the [Integrate authentication into Next.js](https://www.ory.sh/docs/getting-started/integrate-auth/nextjs) documentation.