From a87f98eb20f6fea7bad561cdd11c1939a740de06 Mon Sep 17 00:00:00 2001 From: aeneasr <3372410+aeneasr@users.noreply.github.com> Date: Tue, 15 Oct 2024 09:46:03 +0200 Subject: [PATCH] feat: add nextjs package --- package-lock.json | 21 +++++++ packages/elements-nextjs/package.json | 54 ++++++++++++++++ .../elements-nextjs/src/router/app/error.ts | 55 ++++++++++++++++ .../elements-nextjs/src/router/app/index.ts | 15 +++++ .../elements-nextjs/src/router/app/login.ts | 62 +++++++++++++++++++ .../src/router/app/recovery.ts | 61 ++++++++++++++++++ .../src/router/app/registration.ts | 61 ++++++++++++++++++ .../src/router/app/settings.ts | 61 ++++++++++++++++++ .../elements-nextjs/src/router/app/utils.ts | 58 +++++++++++++++++ .../src/router/app/verification.ts | 59 ++++++++++++++++++ packages/elements-nextjs/tsconfig.json | 35 +++++++++++ 11 files changed, 542 insertions(+) create mode 100644 packages/elements-nextjs/package.json create mode 100644 packages/elements-nextjs/src/router/app/error.ts create mode 100644 packages/elements-nextjs/src/router/app/index.ts create mode 100644 packages/elements-nextjs/src/router/app/login.ts create mode 100644 packages/elements-nextjs/src/router/app/recovery.ts create mode 100644 packages/elements-nextjs/src/router/app/registration.ts create mode 100644 packages/elements-nextjs/src/router/app/settings.ts create mode 100644 packages/elements-nextjs/src/router/app/utils.ts create mode 100644 packages/elements-nextjs/src/router/app/verification.ts create mode 100644 packages/elements-nextjs/tsconfig.json diff --git a/package-lock.json b/package-lock.json index b473555d6..aa46e9e0e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7361,6 +7361,10 @@ "resolved": "packages/markup", "link": true }, + "node_modules/@ory/elements-nextjs": { + "resolved": "packages/elements-nextjs", + "link": true + }, "node_modules/@ory/elements-preact": { "resolved": "packages/preact", "link": true @@ -34194,6 +34198,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/elements-nextjs": { + "version": "1.0.0-next.10", + "license": "Apache License 2.0", + "dependencies": { + "@ory/client-fetch": "^1.15.4" + }, + "devDependencies": {}, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + } + }, + "packages/elements-nextjs/node_modules/@ory/client-fetch": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/@ory/client-fetch/-/client-fetch-1.15.6.tgz", + "integrity": "sha512-etWhiJC5kF/qbXugAtUHGi+8Z8L/v+dKXzkRA7jy90tlfSTVem+rmwCPtaH3KaLaYWQ4ltAjOh0LEOCetrE2sQ==" + }, "packages/elements-react": { "name": "@ory/elements-react", "version": "1.0.0-next.10", diff --git a/packages/elements-nextjs/package.json b/packages/elements-nextjs/package.json new file mode 100644 index 000000000..516bdb8dc --- /dev/null +++ b/packages/elements-nextjs/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ory/elements-nextjs", + "version": "1.0.0-next.10", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "typesVersions": { + "*": { + "index": [ + "./dist/index.d.ts" + ] + } + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "dependencies": { + "@ory/client-fetch": "^1.15.4" + }, + "peerDependencies": { + "react": "18.3.1", + "react-dom": "18.3.1" + }, + "devDependencies": { + }, + "keywords": [ + "ory", + "auth", + "nextjs", + "react", + "passwordless", + "authentication", + "passkeys" + ], + "repository": { + "type": "git", + "url": "git+ssh://git@github.com/ory/elements.git", + "directory": "packages/elements-nextjs" + }, + "bugs": { + "url": "https://github.com/ory/elements/issues" + }, + "homepage": "https://github.com/ory/elements#readme", + "author": "Ory Corp", + "license": "Apache License 2.0", + "description": "Ory Elements React - a collection of React components for authentication UIs.", + "publishConfig": { + "access": "public" + } +} diff --git a/packages/elements-nextjs/src/router/app/error.ts b/packages/elements-nextjs/src/router/app/error.ts new file mode 100644 index 000000000..c79ddacac --- /dev/null +++ b/packages/elements-nextjs/src/router/app/error.ts @@ -0,0 +1,55 @@ +import { init, initOverrides, onRedirect, QueryParams, toValue } from "./utils" +import { FlowType, FlowError, handleFlowError ,FrontendApi} from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing flow error. This method works with server-side rendering. + * + * ``` + * import { getOrCreateErrorFlow } from "$/elements/frameworks/nextjs/routers/app/error" + * import { Error } from "$/elements/headless/flows/error" + * + * export default async function ErrorPage({ searchParams }: PageProps) { + * const flow = await getFlowError(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export function getFlowError(client: FrontendApi): (params: QueryParams) => Promise { + return async (params: QueryParams) => { + // Restart in an error scenario happens when the error does not exist or is expired. In that case there is + // nothing to do but try to sign in, which will redirect us if we're already logged in. + const onRestartFlow = () => init(params, FlowType.Login) + if (!params.id) { + return onRestartFlow() + } + + try { + const response = await client.getFlowErrorRaw( + { + id: params.id, + }, + initOverrides, + ) + return toValue(response) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError: () => onRestartFlow(), + onRestartFlow, + onRedirect, + }) + await errorHandler(error) + return null + } + } +} diff --git a/packages/elements-nextjs/src/router/app/index.ts b/packages/elements-nextjs/src/router/app/index.ts new file mode 100644 index 000000000..ff21ada85 --- /dev/null +++ b/packages/elements-nextjs/src/router/app/index.ts @@ -0,0 +1,15 @@ +import { getFlowError } from "./error" +import { getOrCreateLoginFlow } from "./login" +import { getOrCreateRecoveryFlow } from "./recovery" +import { getOrCreateRegistrationFlow } from "./registration" +import { getOrCreateSettingsFlow } from "./settings" +import { getOrCreateVerificationFlow } from "./verification" + +export { + getFlowError, + getOrCreateLoginFlow, + getOrCreateRecoveryFlow, + getOrCreateRegistrationFlow, + getOrCreateSettingsFlow, + getOrCreateVerificationFlow, +} diff --git a/packages/elements-nextjs/src/router/app/login.ts b/packages/elements-nextjs/src/router/app/login.ts new file mode 100644 index 000000000..75f90235d --- /dev/null +++ b/packages/elements-nextjs/src/router/app/login.ts @@ -0,0 +1,62 @@ +import { + init, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + requestParams, + toValue, +} from "$/elements/frameworks/nextjs/routers/app/utils" +import { serverClientFrontend } from "$/utils/sdk" +import { LoginFlow } from "@ory-corp/client" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing login flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { getOrCreateLoginFlow } from "$/elements/frameworks/nextjs/routers/app/login" + * import { Login } from "$/elements/headless/flows/login" + * + * export default async function LoginPage({ searchParams }: PageProps) { + * const flow = await getOrCreateLoginFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getOrCreateLoginFlow( + params: QueryParams, +): Promise { + const onRestartFlow = () => init(params, FlowType.Login) + if (!params.flow) { + return onRestartFlow() + } + + try { + const resp = await serverClientFrontend().getLoginFlowRaw( + requestParams(params), + initOverrides, + ) + + return toValue(resp) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + errorHandler(error) + return null + } +} diff --git a/packages/elements-nextjs/src/router/app/recovery.ts b/packages/elements-nextjs/src/router/app/recovery.ts new file mode 100644 index 000000000..4dd07e502 --- /dev/null +++ b/packages/elements-nextjs/src/router/app/recovery.ts @@ -0,0 +1,61 @@ +import { + init, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + requestParams, + toValue, +} from "$/elements/frameworks/nextjs/routers/app/utils" +import { serverClientFrontend } from "$/utils/sdk" +import { RecoveryFlow } from "@ory-corp/client" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing recovery flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { getOrCreateRecoveryFlow } from "$/elements/frameworks/nextjs/routers/app/recovery" + * import { Recovery } from "$/elements/headless/flows/recovery" + * + * export default async function RecoveryPage({ searchParams }: PageProps) { + * const flow = await getOrCreateRecoveryFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getOrCreateRecoveryFlow( + params: QueryParams, +): Promise { + const onRestartFlow = () => init(params, FlowType.Recovery) + if (!params.flow) { + return onRestartFlow() + } + + try { + const response = await serverClientFrontend().getRecoveryFlowRaw( + requestParams(params), + initOverrides, + ) + return toValue(response) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + errorHandler(error) + return null + } +} diff --git a/packages/elements-nextjs/src/router/app/registration.ts b/packages/elements-nextjs/src/router/app/registration.ts new file mode 100644 index 000000000..215afbc2f --- /dev/null +++ b/packages/elements-nextjs/src/router/app/registration.ts @@ -0,0 +1,61 @@ +import { + init, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + requestParams, + toValue, +} from "$/elements/frameworks/nextjs/routers/app/utils" +import { serverClientFrontend } from "$/utils/sdk" +import { RegistrationFlow } from "@ory-corp/client" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing registration flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { getOrCreateRegistrationFlow } from "$/elements/frameworks/nextjs/routers/app/registration" + * import { Registration } from "$/elements/headless/flows/registration" + * + * export default async function RegistrationPage({ searchParams }: PageProps) { + * const flow = await getOrCreateRegistrationFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getOrCreateRegistrationFlow( + params: QueryParams, +): Promise { + const onRestartFlow = () => init(params, FlowType.Registration) + if (!params.flow) { + return onRestartFlow() + } + + try { + const response = await serverClientFrontend().getRegistrationFlowRaw( + requestParams(params), + initOverrides, + ) + return toValue(response) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + errorHandler(error) + return null + } +} diff --git a/packages/elements-nextjs/src/router/app/settings.ts b/packages/elements-nextjs/src/router/app/settings.ts new file mode 100644 index 000000000..81229249d --- /dev/null +++ b/packages/elements-nextjs/src/router/app/settings.ts @@ -0,0 +1,61 @@ +import { + init, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + requestParams, + toValue, +} from "$/elements/frameworks/nextjs/routers/app/utils" +import { serverClientFrontend } from "$/utils/sdk" +import { SettingsFlow } from "@ory-corp/client" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing settings flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { getOrCreateSettingsFlow } from "$/elements/frameworks/nextjs/routers/app/settings" + * import { Settings } from "$/elements/headless/flows/settings" + * + * export default async function SettingsPage({ searchParams }: PageProps) { + * const flow = await getOrCreateSettingsFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getOrCreateSettingsFlow( + params: QueryParams, +): Promise { + const onRestartFlow = () => init(params, FlowType.Settings) + if (!params.flow) { + return onRestartFlow() + } + + try { + const response = await serverClientFrontend().getSettingsFlowRaw( + requestParams(params), + initOverrides, + ) + return toValue(response) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + onRestartFlow, + onRedirect, + }) + errorHandler(error) + return null + } +} diff --git a/packages/elements-nextjs/src/router/app/utils.ts b/packages/elements-nextjs/src/router/app/utils.ts new file mode 100644 index 000000000..7e7d1b848 --- /dev/null +++ b/packages/elements-nextjs/src/router/app/utils.ts @@ -0,0 +1,58 @@ +import { getCookieHeader } from "$/utils/headers" +import { ApiResponse } from "@ory-corp/client" +import { FlowType, handleFlowError, OnRedirectHandler } from "@ory/client-fetch" +import { redirect, RedirectType } from "next/navigation" + +export type QueryParams = { [key: string]: any } + +export const initOverrides: RequestInit = { + cache: "no-cache", +} + +export function toValue(res: ApiResponse) { + return res.value() +} + +export const onRedirect: OnRedirectHandler = (url, external) => { + redirect(url) +} + +export function onValidationError(value: T): T { + return value +} + +export type ParsedQueryParams = { + id: string + cookie: string | undefined + return_to: string +} + +export function requestParams(params: QueryParams): ParsedQueryParams { + return { + id: params.flow, + cookie: getCookieHeader(), + return_to: params.return_to, + } +} + +export function init(params: QueryParams, flowType: FlowType) { + // Take advantage of the fact, that Ory handles the flow creation for us and redirects the user to the default + // return to automatically if they're logged in already. + return redirect( + "/self-service/" + + flowType.toString() + + "/browser?" + + new URLSearchParams(params).toString(), + RedirectType.replace, + ) +} + +export const onError = (onRestartFlow: () => void) => (err: any) => + new Promise((resolve) => { + handleFlowError({ + onValidationError: resolve, + // RestartFlow and Redirect both use redirects hence we don't need to resolve here. + onRestartFlow, + onRedirect, + })(err) + }) diff --git a/packages/elements-nextjs/src/router/app/verification.ts b/packages/elements-nextjs/src/router/app/verification.ts new file mode 100644 index 000000000..8b914d714 --- /dev/null +++ b/packages/elements-nextjs/src/router/app/verification.ts @@ -0,0 +1,59 @@ +import { + init, + initOverrides, + onRedirect, + onValidationError, + QueryParams, + requestParams, + toValue, +} from "$/elements/frameworks/nextjs/routers/app/utils" +import { serverClientFrontend } from "$/utils/sdk" +import { FlowType, handleFlowError } from "@ory/client-fetch" + +/** + * Use this method in an app router page to fetch an existing verification flow or to create a new one. This method works with server-side rendering. + * + * ``` + * import { getOrCreateVerificationFlow } from "$/elements/frameworks/nextjs/routers/app/verification" + * import { Verification } from "$/elements/headless/flows/verification" + * + * export default async function VerificationPage({ searchParams }: PageProps) { + * const flow = await getOrCreateVerificationFlow(searchParams) + * + * return ( + * .projects.oryapis.com" }, + * }} + * /> + * ) + * } + * ``` + * + * @param params The query parameters of the request. + */ +export async function getOrCreateVerificationFlow(params: QueryParams) { + const onRestartFlow = () => init(params, FlowType.Verification) + if (!params.flow) { + return onRestartFlow() + } + + try { + const response = await serverClientFrontend().getVerificationFlowRaw( + requestParams(params), + initOverrides, + ) + return toValue(response) + } catch (error) { + const errorHandler = handleFlowError({ + onValidationError, + // RestartFlow and Redirect both use redirects hence we don't need to resolve here. + onRestartFlow, + onRedirect, + }) + errorHandler(error) + return null + } +} diff --git a/packages/elements-nextjs/tsconfig.json b/packages/elements-nextjs/tsconfig.json new file mode 100644 index 000000000..106512be5 --- /dev/null +++ b/packages/elements-nextjs/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "target": "ES2019", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "moduleResolution": "Bundler", + "module": "ESNext", + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true, + "outDir": "dist", + "resolveJsonModule": true, + "declarationDir": "dist/types", + "jsx": "react-jsx", + "lib": ["ES6", "DOM", "WebWorker"], + "rootDir": "src", + "paths": { + "@ory/elements-react": ["./src/index.ts"], + "@tests/*": ["./src/tests/*"] + } + }, + "exclude": ["node_modules"], + "include": [ + "src/**/*.ts", + "src/**/*.tsx", + "src/global.d.ts", + "src/tests/**/*.ts", + "src/tests/**/*.tsx", + "tsup.config.ts" + ] +}