diff --git a/.pnp.cjs b/.pnp.cjs index 58a8ec27..845c0180 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -275,7 +275,7 @@ const RAW_RUNTIME_STATE = ["@types/react", "npm:18.2.40"],\ ["@types/react-dom", "npm:18.2.10"],\ ["@types/tldjs", "npm:2.3.4"],\ - ["axios", "npm:1.5.1"],\ + ["axios", "npm:1.7.7"],\ ["next", "virtual:a29650b7eaad3692d1139d6ca50163f9c611de4f12c1b46473850e285251c23b9c7999b6fe9d59b5db61d9d37958f2e8c07243c0c5bbcd15ae669fb338f0d523#npm:14.2.9"],\ ["react", "npm:18.3.1"],\ ["react-dom", "virtual:a29650b7eaad3692d1139d6ca50163f9c611de4f12c1b46473850e285251c23b9c7999b6fe9d59b5db61d9d37958f2e8c07243c0c5bbcd15ae669fb338f0d523#npm:18.3.1"],\ @@ -3602,16 +3602,6 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["axios", [\ - ["npm:1.5.1", {\ - "packageLocation": "../.yarn/berry/cache/axios-npm-1.5.1-6bc68e7d25-10.zip/node_modules/axios/",\ - "packageDependencies": [\ - ["axios", "npm:1.5.1"],\ - ["follow-redirects", "virtual:cfbedc233d4c16068d815547ad303dec1092fdb3b8bb4ec9ab9c56bdd55b4e87650c7a525a88805756f4d2819c03abfd96a9983cfa927fedf995d1b8b879db38#npm:1.15.9"],\ - ["form-data", "npm:4.0.0"],\ - ["proxy-from-env", "npm:1.1.0"]\ - ],\ - "linkType": "HARD"\ - }],\ ["npm:1.7.7", {\ "packageLocation": "../.yarn/berry/cache/axios-npm-1.7.7-cfbedc233d-10.zip/node_modules/axios/",\ "packageDependencies": [\ diff --git a/.yarnrc.yml b/.yarnrc.yml index 5a723690..a6838ed3 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -5,3 +5,9 @@ enableGlobalCache: true globalFolder: ../.yarn/berry yarnPath: .yarn/releases/yarn.cjs + +preferReuse: true + +defaultSemverRangePrefix: '' + +pnpEnableEsmLoader: true diff --git a/packages/identity-integration/README.md b/packages/identity-integration/README.md index 19e9d5b3..4eb33731 100644 --- a/packages/identity-integration/README.md +++ b/packages/identity-integration/README.md @@ -1,5 +1,11 @@ # Identity Integration +## BREAKING CHANGE 1.0.0 + +- Flow экспортируются: + - `@atls/next-identity-integration/app-router` - для `app` роутера + - `@atls/next-identity-integration/page-router` - для `pages` роутера + ## BREAKING CHANGE 0.2.0 - Переход на `App Router` для `Next.JS@14` diff --git a/packages/identity-integration/package.json b/packages/identity-integration/package.json index 15ebc29e..40c8aa18 100644 --- a/packages/identity-integration/package.json +++ b/packages/identity-integration/package.json @@ -4,7 +4,10 @@ "license": "BSD-3-Clause", "type": "module", "exports": { - ".": "./src/index.ts" + ".": "./src/index.ts", + "./package.json": "./package.json", + "./app-router": "./src/app-router.ts", + "./page-router": "./src/page-router.ts" }, "main": "src/index.ts", "files": [ @@ -25,7 +28,7 @@ "@types/react": "18.2.40", "@types/react-dom": "18.2.10", "@types/tldjs": "2.3.4", - "axios": "1.5.1", + "axios": "1.7.7", "next": "14.2.9", "react": "18.3.1", "react-dom": "18.3.1" @@ -36,6 +39,24 @@ }, "publishConfig": { "access": "public", + "exports": { + "./package.json": "./package.json", + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./app-router": { + "import": "./dist/app-router.js", + "types": "./dist/app-router.d.ts", + "default": "./dist/app-router.js" + }, + "./page-router": { + "import": "./dist/page-router.js", + "types": "./dist/page-router.d.ts", + "default": "./dist/page-router.js" + } + }, "main": "dist/index.js", "typings": "dist/index.d.ts" }, diff --git a/packages/identity-integration/src/app-router.ts b/packages/identity-integration/src/app-router.ts new file mode 100644 index 00000000..a1c7a332 --- /dev/null +++ b/packages/identity-integration/src/app-router.ts @@ -0,0 +1 @@ +export * from './flows-app-router/index.js' diff --git a/packages/identity-integration/src/flows/error.flow.tsx b/packages/identity-integration/src/flows-app-router/error.flow.tsx similarity index 64% rename from packages/identity-integration/src/flows/error.flow.tsx rename to packages/identity-integration/src/flows-app-router/error.flow.tsx index 45a20d47..c8c2004d 100644 --- a/packages/identity-integration/src/flows/error.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/error.flow.tsx @@ -1,15 +1,16 @@ -import { FlowError } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useRouter } from 'next/navigation.js' -import { useSearchParams } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import React from 'react' - -import { ErrorProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' +import type { FlowError } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/navigation.js' +import { useSearchParams } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import React from 'react' + +import { ErrorProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' export interface ErrorErrorProps { returnToUrl?: string diff --git a/packages/identity-integration/src/flows/handle-errors.util.ts b/packages/identity-integration/src/flows-app-router/handle-errors.util.ts similarity index 100% rename from packages/identity-integration/src/flows/handle-errors.util.ts rename to packages/identity-integration/src/flows-app-router/handle-errors.util.ts diff --git a/packages/identity-integration/src/flows/index.ts b/packages/identity-integration/src/flows-app-router/index.ts similarity index 100% rename from packages/identity-integration/src/flows/index.ts rename to packages/identity-integration/src/flows-app-router/index.ts diff --git a/packages/identity-integration/src/flows/login.flow.tsx b/packages/identity-integration/src/flows-app-router/login.flow.tsx similarity index 72% rename from packages/identity-integration/src/flows/login.flow.tsx rename to packages/identity-integration/src/flows-app-router/login.flow.tsx index 4fc9a57d..8dc069e2 100644 --- a/packages/identity-integration/src/flows/login.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/login.flow.tsx @@ -1,22 +1,23 @@ -import { UpdateLoginFlowBody } from '@ory/kratos-client' -import { LoginFlow as KratosLoginFlow } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import React from 'react' - -import { FlowProvider } from '../providers/index.js' -import { ValuesProvider } from '../providers/index.js' -import { ValuesStore } from '../providers/index.js' -import { SubmitProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' -import { handleFlowError } from './handle-errors.util.js' +import type { UpdateLoginFlowBody } from '@ory/kratos-client' +import type { LoginFlow as KratosLoginFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' export interface LoginFlowProps { onError?: (error: { id: string }) => void diff --git a/packages/identity-integration/src/flows/logout.flow.tsx b/packages/identity-integration/src/flows-app-router/logout.flow.tsx similarity index 66% rename from packages/identity-integration/src/flows/logout.flow.tsx rename to packages/identity-integration/src/flows-app-router/logout.flow.tsx index cf8cf636..1650abc7 100644 --- a/packages/identity-integration/src/flows/logout.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/logout.flow.tsx @@ -1,14 +1,15 @@ -import { LogoutFlow as KratosLogoutFlow } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import React from 'react' - -import { useKratosClient } from '../providers/index.js' +import type { LogoutFlow as KratosLogoutFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import React from 'react' + +import { useKratosClient } from '../providers/index.js' interface LogoutFlowProps { returnToUrl?: string diff --git a/packages/identity-integration/src/flows/recovery.flow.tsx b/packages/identity-integration/src/flows-app-router/recovery.flow.tsx similarity index 71% rename from packages/identity-integration/src/flows/recovery.flow.tsx rename to packages/identity-integration/src/flows-app-router/recovery.flow.tsx index 43b4c7e4..6657ba1a 100644 --- a/packages/identity-integration/src/flows/recovery.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/recovery.flow.tsx @@ -1,22 +1,23 @@ -import { UpdateRecoveryFlowBody } from '@ory/kratos-client' -import { RecoveryFlow as KratosRecoveryFlow } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import React from 'react' - -import { FlowProvider } from '../providers/index.js' -import { ValuesProvider } from '../providers/index.js' -import { ValuesStore } from '../providers/index.js' -import { SubmitProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' -import { handleFlowError } from './handle-errors.util.js' +import type { UpdateRecoveryFlowBody } from '@ory/kratos-client' +import type { RecoveryFlow as KratosRecoveryFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' export interface RecoveryFlowProps { onError?: (error: { id: string }) => void diff --git a/packages/identity-integration/src/flows/registration.flow.tsx b/packages/identity-integration/src/flows-app-router/registration.flow.tsx similarity index 74% rename from packages/identity-integration/src/flows/registration.flow.tsx rename to packages/identity-integration/src/flows-app-router/registration.flow.tsx index 66f45a73..57a570ba 100644 --- a/packages/identity-integration/src/flows/registration.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/registration.flow.tsx @@ -1,25 +1,26 @@ -import { Identity } from '@ory/kratos-client' -import { UpdateRegistrationFlowBody } from '@ory/kratos-client' -import { RegistrationFlow as KratosRegistrationFlow } from '@ory/kratos-client' -import { ContinueWith as KratosContinueWith } from '@ory/kratos-client' -import { UiNodeInputAttributes } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import React from 'react' - -import { FlowProvider } from '../providers/index.js' -import { ValuesProvider } from '../providers/index.js' -import { ValuesStore } from '../providers/index.js' -import { SubmitProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' -import { handleFlowError } from './handle-errors.util.js' +import type { Identity } from '@ory/kratos-client' +import type { UpdateRegistrationFlowBody } from '@ory/kratos-client' +import type { RegistrationFlow as KratosRegistrationFlow } from '@ory/kratos-client' +import type { ContinueWith as KratosContinueWith } from '@ory/kratos-client' +import type { UiNodeInputAttributes } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' export interface RegistrationFlowProps { onError?: (error: { id: string }) => void diff --git a/packages/identity-integration/src/flows/settings.flow.tsx b/packages/identity-integration/src/flows-app-router/settings.flow.tsx similarity index 73% rename from packages/identity-integration/src/flows/settings.flow.tsx rename to packages/identity-integration/src/flows-app-router/settings.flow.tsx index 4e44dff7..e58660d5 100644 --- a/packages/identity-integration/src/flows/settings.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/settings.flow.tsx @@ -1,22 +1,23 @@ -import { UpdateSettingsFlowBody } from '@ory/kratos-client' -import { SettingsFlow as KratosSettingsFlow } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import React from 'react' - -import { FlowProvider } from '../providers/index.js' -import { ValuesProvider } from '../providers/index.js' -import { ValuesStore } from '../providers/index.js' -import { SubmitProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' -import { handleFlowError } from './handle-errors.util.js' +import type { UpdateSettingsFlowBody } from '@ory/kratos-client' +import type { SettingsFlow as KratosSettingsFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' export interface SettingsFlowProps { onError?: (error: { id: string }) => void diff --git a/packages/identity-integration/src/flows/verification.flow.tsx b/packages/identity-integration/src/flows-app-router/verification.flow.tsx similarity index 70% rename from packages/identity-integration/src/flows/verification.flow.tsx rename to packages/identity-integration/src/flows-app-router/verification.flow.tsx index 036912ac..a8b4866c 100644 --- a/packages/identity-integration/src/flows/verification.flow.tsx +++ b/packages/identity-integration/src/flows-app-router/verification.flow.tsx @@ -1,23 +1,24 @@ /* eslint-disable default-case */ -import { UpdateVerificationFlowBody } from '@ory/kratos-client' -import { VerificationFlow as KratosVerificationFlow } from '@ory/kratos-client' -import { AxiosError } from 'axios' -import { PropsWithChildren } from 'react' -import { FC } from 'react' -import { useSearchParams } from 'next/navigation.js' -import { useRouter } from 'next/navigation.js' -import { useState } from 'react' -import { useEffect } from 'react' -import { useMemo } from 'react' -import { useCallback } from 'react' -import React from 'react' - -import { FlowProvider } from '../providers/index.js' -import { ValuesProvider } from '../providers/index.js' -import { ValuesStore } from '../providers/index.js' -import { SubmitProvider } from '../providers/index.js' -import { useKratosClient } from '../providers/index.js' +import type { UpdateVerificationFlowBody } from '@ory/kratos-client' +import type { VerificationFlow as KratosVerificationFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useSearchParams } from 'next/navigation.js' +import { useRouter } from 'next/navigation.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' export interface VerificationFlowProps { onError?: (error: { id: string }) => void diff --git a/packages/identity-integration/src/flows-page-router/error.flow.tsx b/packages/identity-integration/src/flows-page-router/error.flow.tsx new file mode 100644 index 00000000..9069494e --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/error.flow.tsx @@ -0,0 +1,53 @@ +import type { FlowError } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import React from 'react' + +import { ErrorProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' + +export interface ErrorErrorProps { + returnToUrl?: string +} + +export const ErrorFlow: FC> = ({ children, returnToUrl }) => { + const [error, setError] = useState() + const [loading, setLoading] = useState(true) + const { push, query, isReady } = useRouter() + + const { kratosClient } = useKratosClient() + + const { id } = query + + useEffect(() => { + if (!isReady || error) { + return + } + + kratosClient + .getFlowError({ id: String(id) }) + .then(({ data }) => { + setError(data) + }) + .catch((err: AxiosError) => { + // eslint-disable-next-line default-case + switch (err.response?.status) { + case 404: + case 403: + case 410: + return push(returnToUrl ?? '/auth/login') + } + + return Promise.reject(err) + }) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id, push, isReady, error]) + + return {children} +} diff --git a/packages/identity-integration/src/flows-page-router/handle-errors.util.ts b/packages/identity-integration/src/flows-page-router/handle-errors.util.ts new file mode 100644 index 00000000..d7c560de --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/handle-errors.util.ts @@ -0,0 +1,89 @@ +/* eslint-disable consistent-return */ +/* eslint-disable prefer-template */ +/* eslint-disable default-case */ + +import type { AxiosError } from 'axios' +import type { NextRouter } from 'next/router.js' +import type { Dispatch } from 'react' +import type { SetStateAction } from 'react' + +export const handleFlowError = ( + router: NextRouter, + flowType: 'login' | 'registration' | 'settings' | 'recovery' | 'verification', + onResetFlow: Dispatch>, + onErrorRedirectUrl: string, + onError?: (error: any) => void + ) => + async (error: AxiosError) => { + const redirectToSettings = onErrorRedirectUrl + + switch (error.response?.data.error?.id) { + case 'session_aal2_required': + window.location.href = error.response?.data.redirect_browser_to + + return + case 'session_already_available': + if (error.response?.data?.redirect_browser_to) { + window.location.href = error.response.data.redirect_browser_to + } else { + await router.push(redirectToSettings) + } + + return + case 'session_refresh_required': + window.location.href = error.response?.data.redirect_browser_to + + return + case 'self_service_flow_return_to_forbidden': + if (onError) { + onError(error.response.data.error) + } + + onResetFlow(undefined) + + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) + + return + case 'self_service_flow_expired': + if (onError) { + onError(error.response.data.error) + } + + onResetFlow(undefined) + + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) + + return + case 'security_csrf_violation': + if (onError) { + onError(error.response.data.error) + } + + onResetFlow(undefined) + + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) + + return + case 'security_identity_mismatch': + onResetFlow(undefined) + + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) + + return + case 'browser_location_change_required': + window.location.href = error.response.data.redirect_browser_to + + return + } + + switch (error.response?.status) { + case 410: + onResetFlow(undefined) + + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) + + return + } + + return Promise.reject(error) + } diff --git a/packages/identity-integration/src/flows-page-router/index.ts b/packages/identity-integration/src/flows-page-router/index.ts new file mode 100644 index 00000000..87bf4646 --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/index.ts @@ -0,0 +1,7 @@ +export * from './verification.flow.js' +export * from './registration.flow.js' +export * from './recovery.flow.js' +export * from './settings.flow.js' +export * from './login.flow.js' +export * from './error.flow.js' +export * from './logout.flow.js' diff --git a/packages/identity-integration/src/flows-page-router/login.flow.tsx b/packages/identity-integration/src/flows-page-router/login.flow.tsx new file mode 100644 index 00000000..63e8920a --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/login.flow.tsx @@ -0,0 +1,125 @@ +import type { UpdateLoginFlowBody } from '@ory/kratos-client' +import type { LoginFlow as KratosLoginFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' + +export interface LoginFlowProps { + onError?: (error: { id: string }) => void + returnToUrl?: string +} + +export const LoginFlow: FC> = ({ + children, + onError, + returnToUrl, +}) => { + const [flow, setFlow] = useState() + const [submitting, setSubmitting] = useState(false) + const [loading, setLoading] = useState(true) + const values = useMemo(() => new ValuesStore(), []) + const router = useRouter() + const { kratosClient, returnToSettingsUrl } = useKratosClient() + + const { return_to: returnTo, flow: flowId, refresh, aal } = router.query + + useEffect(() => { + if (!router.isReady || flow) return + + if (flowId) { + kratosClient + .getLoginFlow({ id: String(flowId) }, { withCredentials: true }) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'login', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + + return + } + + kratosClient + .createBrowserLoginFlow( + { + refresh: Boolean(refresh), + aal: aal ? String(aal) : undefined, + returnTo: returnTo?.toString() ?? returnToUrl, + }, + { withCredentials: true } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'login', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flowId, router, router.isReady, aal, refresh, returnTo, flow, onError]) + + useEffect(() => { + if (flow) { + values.setFromFlow(flow) + } + }, [values, flow]) + + const onSubmit = useCallback( + (override?: Partial) => { + setSubmitting(true) + + const body = { + ...(values.getValues() as UpdateLoginFlowBody), + ...(override || {}), + } + + kratosClient + .updateLoginFlow( + // @ts-ignore + { flow: String(flow?.id), updateLoginFlowBody: body }, + { withCredentials: true } + ) + .then(() => { + if (flow?.return_to) { + window.location.href = flow?.return_to + } else { + router.push(returnToUrl ?? '/') + } + }) + .catch(handleFlowError(router, 'login', setFlow, returnToSettingsUrl)) + .catch((error: AxiosError) => { + if (error.response?.status === 400) { + setFlow(error.response?.data) + + return + } + + // eslint-disable-next-line consistent-return + return Promise.reject(error) + }) + .finally(() => setSubmitting(false)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [router, flow, values, setSubmitting] + ) + + return ( + + + {/* @ts-ignore */} + {children} + + + ) +} diff --git a/packages/identity-integration/src/flows-page-router/logout.flow.tsx b/packages/identity-integration/src/flows-page-router/logout.flow.tsx new file mode 100644 index 00000000..dd5a14fe --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/logout.flow.tsx @@ -0,0 +1,58 @@ +import type { LogoutFlow as KratosLogoutFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import React from 'react' + +import { useKratosClient } from '../providers/index.js' + +interface LogoutFlowProps { + returnToUrl?: string +} + +export const LogoutFlow: FC> = ({ children, returnToUrl }) => { + const [logoutToken, setLogoutToken] = useState('') + const router = useRouter() + const { kratosClient } = useKratosClient() + + const { return_to: returnTo } = router.query + + useEffect(() => { + if (!router.isReady) return + + kratosClient + .createBrowserLogoutFlow( + { returnTo: returnTo?.toString() ?? returnToUrl }, + { withCredentials: true } + ) + .then(({ data }) => { + setLogoutToken(data.logout_token) + }) + .catch((error: AxiosError) => { + // eslint-disable-next-line default-case + switch (error.response?.status) { + case 401: + return router.push('/auth/login') + } + + return Promise.reject(error) + }) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [router]) + + useEffect(() => { + if (logoutToken) { + kratosClient + .updateLogoutFlow({ token: logoutToken }, { withCredentials: true }) + .then(() => router.reload()) + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [logoutToken, router]) + + // eslint-disable-next-line react/jsx-no-useless-fragment + return <>{children} +} diff --git a/packages/identity-integration/src/flows-page-router/recovery.flow.tsx b/packages/identity-integration/src/flows-page-router/recovery.flow.tsx new file mode 100644 index 00000000..3a39eb50 --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/recovery.flow.tsx @@ -0,0 +1,129 @@ +import type { UpdateRecoveryFlowBody } from '@ory/kratos-client' +import type { RecoveryFlow as KratosRecoveryFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' + +export interface RecoveryFlowProps { + onError?: (error: { id: string }) => void + returnToUrl?: string +} + +export const RecoveryFlow: FC> = ({ + children, + onError, + returnToUrl, +}) => { + const [flow, setFlow] = useState() + const [submitting, setSubmitting] = useState(false) + const [loading, setLoading] = useState(true) + const values = useMemo(() => new ValuesStore(), []) + const router = useRouter() + const { kratosClient, returnToSettingsUrl } = useKratosClient() + + const { return_to: returnTo, flow: flowId, refresh, aal } = router.query + + useEffect(() => { + if (!router.isReady || flow) return + + if (flowId) { + kratosClient + .getRecoveryFlow({ id: String(flowId) }, { withCredentials: true }) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + + return + } + + kratosClient + .createBrowserRecoveryFlow( + { returnTo: returnTo?.toString() ?? returnToUrl }, + { + withCredentials: true, + } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl, onError)) + .catch((error: AxiosError) => { + if (error.response?.status === 400) { + setFlow(error.response?.data) + + return + } + + // eslint-disable-next-line consistent-return + return Promise.reject(error) + }) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flowId, router, router.isReady, aal, refresh, returnTo, flow, onError]) + + useEffect(() => { + if (flow) { + values.setFromFlow(flow) + } + }, [values, flow]) + + const onSubmit = useCallback( + (override?: Partial) => { + setSubmitting(true) + + const body = { + ...(values.getValues() as UpdateRecoveryFlowBody), + ...(override || {}), + } + + kratosClient + .updateRecoveryFlow( + // @ts-ignore + { flow: String(flow?.id), updateRecoveryFlowBody: body }, + { withCredentials: true } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl)) + .catch((error: AxiosError) => { + if (error.response?.status === 400) { + setFlow(error.response?.data) + + return + } + + // eslint-disable-next-line consistent-return + return Promise.reject(error) + }) + .finally(() => setSubmitting(false)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [router, flow, values, setSubmitting] + ) + + return ( + + + {/* @ts-ignore Enum conflict with string */} + {children} + + + ) +} diff --git a/packages/identity-integration/src/flows-page-router/registration.flow.tsx b/packages/identity-integration/src/flows-page-router/registration.flow.tsx new file mode 100644 index 00000000..ae6db432 --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/registration.flow.tsx @@ -0,0 +1,181 @@ +import type { Identity } from '@ory/kratos-client' +import type { UpdateRegistrationFlowBody } from '@ory/kratos-client' +import type { RegistrationFlow as KratosRegistrationFlow } from '@ory/kratos-client' +import type { ContinueWith as KratosContinueWith } from '@ory/kratos-client' +import type { UiNodeInputAttributes } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' + +export interface RegistrationFlowProps { + onError?: (error: { id: string }) => void + returnToUrl?: string + shouldRedirect?: boolean + passEmail: boolean +} + +type ContinueWith = KratosContinueWith & { + flow?: { + id: string + url?: string + verifiable_address?: string + } +} + +export const RegistrationFlow: FC> = ({ + children, + onError, + returnToUrl, + shouldRedirect = true, + passEmail = false, +}) => { + const [flow, setFlow] = useState() + const [identity, setIdentity] = useState() + const [isValid, setIsValid] = useState(false) + const [submitting, setSubmitting] = useState(false) + const [loading, setLoading] = useState(true) + const values = useMemo(() => new ValuesStore(), []) + const router = useRouter() + const { kratosClient, returnToSettingsUrl } = useKratosClient() + + const { return_to: returnTo, flow: flowId, refresh, aal } = router.query + + useEffect(() => { + if (!router.isReady || flow) return + + if (flowId) { + kratosClient + .getRegistrationFlow({ id: String(flowId) }, { withCredentials: true }) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'registration', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + + return + } + + kratosClient + .createBrowserRegistrationFlow( + { returnTo: shouldRedirect ? (returnTo?.toString() ?? returnToUrl) : undefined }, + { + withCredentials: true, + } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'registration', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flowId, router, router.isReady, aal, refresh, returnTo, flow, onError]) + + useEffect(() => { + if (flow) { + values.setFromFlow(flow) + } + }, [values, flow]) + + const onSubmit = useCallback( + (override?: Partial) => { + setSubmitting(true) + + const [submitNode] = [ + flow?.ui.nodes.filter( + ({ attributes, group }) => + group === 'password' && (attributes as UiNodeInputAttributes).type === 'submit' + ), + flow?.ui.nodes.filter( + ({ attributes }) => (attributes as UiNodeInputAttributes).type === 'submit' + ), + ].flat() + + const body = { + ...(values.getValues() as UpdateRegistrationFlowBody), + ...(submitNode + ? { + [(submitNode.attributes as UiNodeInputAttributes).name]: ( + submitNode.attributes as UiNodeInputAttributes + ).value, + } + : {}), + ...(override || {}), + } + + kratosClient + .updateRegistrationFlow( + // @ts-ignore + { flow: String(flow?.id), updateRegistrationFlowBody: body }, + { withCredentials: true } + ) + .then(async ({ data }) => { + setIdentity(data.identity) + setIsValid(true) + + const continueWithAction: ContinueWith | undefined = data.continue_with?.find( + (action) => action.action === 'show_verification_ui' + ) + + if (flow?.return_to) { + window.location.href = flow?.return_to + } else if (shouldRedirect) { + if (returnToUrl) { + router.push(returnToUrl) + } + if (continueWithAction?.flow?.url) { + if (passEmail) { + const url = new URL(continueWithAction.flow.url) + const params = url.searchParams + const email = continueWithAction.flow.verifiable_address + if (email) params.set('email', email) + + const newUrlString = `${url.origin}${url.pathname}?${params.toString()}` + router.push(newUrlString) + } else { + router.push(continueWithAction.flow.url) + } + } else { + router.push('/') + } + } + }) + .catch(handleFlowError(router, 'registration', setFlow, returnToSettingsUrl)) + .catch((error: AxiosError) => { + if (error.response?.status === 400) { + setFlow(error.response?.data) + + return + } + + // eslint-disable-next-line consistent-return + return Promise.reject(error) + }) + .finally(() => setSubmitting(false)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [router, flow, values, setSubmitting] + ) + + return ( + + + {/* @ts-ignore */} + {children} + + + ) +} diff --git a/packages/identity-integration/src/flows-page-router/settings.flow.tsx b/packages/identity-integration/src/flows-page-router/settings.flow.tsx new file mode 100644 index 00000000..05c8ad0c --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/settings.flow.tsx @@ -0,0 +1,139 @@ +import type { UpdateSettingsFlowBody } from '@ory/kratos-client' +import type { SettingsFlow as KratosSettingsFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' +import { handleFlowError } from './handle-errors.util.js' + +export interface SettingsFlowProps { + onError?: (error: { id: string }) => void + returnToUrl?: string +} + +export const SettingsFlow: FC> = ({ + children, + onError, + returnToUrl, +}) => { + const [flow, setFlow] = useState() + const [submitting, setSubmitting] = useState(false) + const [loading, setLoading] = useState(true) + const values = useMemo(() => new ValuesStore(), []) + const router = useRouter() + const { kratosClient, returnToSettingsUrl } = useKratosClient() + + const { return_to: returnTo, flow: flowId, refresh, aal } = router.query + + useEffect(() => { + if (!router.isReady || flow) return + + if (flowId) { + kratosClient + .getSettingsFlow({ id: String(flowId) }, { withCredentials: true }) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl, onError)) + .finally(() => setLoading(false)) + + return + } + + kratosClient + .createBrowserSettingsFlow( + { returnTo: returnTo?.toString() ?? returnToUrl }, + { + withCredentials: true, + } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl, onError)) + .catch((error: AxiosError) => { + // eslint-disable-next-line default-case + switch (error.response?.status) { + case 401: + if (error.response.data.return_to) { + window.location.href = error.response.data.return_to + } else { + return router.push('/auth/login') + } + } + + return Promise.reject(error) + }) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flowId, router, router.isReady, aal, refresh, returnTo, flow, onError]) + + useEffect(() => { + if (flow) { + values.setFromFlow(flow) + } + }, [values, flow]) + + const onSubmit = useCallback( + (override?: Partial) => { + setSubmitting(true) + + const body = { + ...(values.getValues() as UpdateSettingsFlowBody), + ...(override || {}), + } + + kratosClient + .updateSettingsFlow( + // @ts-ignore + { flow: String(flow?.id), updateSettingsFlowBody: body }, + { withCredentials: true } + ) + .then(({ data }) => { + setFlow(data) + if (flow?.return_to) { + window.location.href = flow?.return_to + } else if (returnToUrl) { + router.push(returnToUrl) + } else { + router.reload() + } + }) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl)) + .catch((error: AxiosError) => { + if (error.response?.status === 400) { + setFlow(error.response?.data) + + return + } + + // eslint-disable-next-line consistent-return + return Promise.reject(error) + }) + .finally(() => setSubmitting(false)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [router, flow, values, setSubmitting] + ) + + return ( + + + {/* @ts-ignore */} + {children} + + + ) +} diff --git a/packages/identity-integration/src/flows-page-router/verification.flow.tsx b/packages/identity-integration/src/flows-page-router/verification.flow.tsx new file mode 100644 index 00000000..068fbd62 --- /dev/null +++ b/packages/identity-integration/src/flows-page-router/verification.flow.tsx @@ -0,0 +1,135 @@ +/* eslint-disable default-case */ + +import type { UpdateVerificationFlowBody } from '@ory/kratos-client' +import type { VerificationFlow as KratosVerificationFlow } from '@ory/kratos-client' +import type { AxiosError } from 'axios' +import type { PropsWithChildren } from 'react' +import type { FC } from 'react' + +import { useRouter } from 'next/router.js' +import { useState } from 'react' +import { useEffect } from 'react' +import { useMemo } from 'react' +import { useCallback } from 'react' +import React from 'react' + +import { FlowProvider } from '../providers/index.js' +import { ValuesProvider } from '../providers/index.js' +import { ValuesStore } from '../providers/index.js' +import { SubmitProvider } from '../providers/index.js' +import { useKratosClient } from '../providers/index.js' + +export interface VerificationFlowProps { + onError?: (error: { id: string }) => void + returnToUrl?: string +} + +export const VerificationFlow: FC> = ({ + children, + onError, + returnToUrl, +}) => { + const [flow, setFlow] = useState() + const [submitting, setSubmitting] = useState(false) + const [loading, setLoading] = useState(true) + const values = useMemo(() => new ValuesStore(), []) + const router = useRouter() + const { kratosClient } = useKratosClient() + + const { return_to: returnTo, flow: flowId, refresh, aal } = router.query + + useEffect(() => { + if (!router.isReady || flow) return + + if (flowId) { + kratosClient + .getVerificationFlow({ id: String(flowId) }, { withCredentials: true }) + .then(({ data }) => { + setFlow(data) + }) + .catch((error: AxiosError) => { + switch (error.response?.status) { + case 410: + case 403: + return router.push('/auth/verification') + } + + throw error + }) + .finally(() => setLoading(false)) + + return + } + + kratosClient + .createBrowserVerificationFlow( + { returnTo: returnTo?.toString() ?? returnToUrl }, + { + withCredentials: true, + } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch((error: AxiosError) => { + switch (error.response?.status) { + case 400: + return router.push('/') + } + + throw error + }) + .finally(() => setLoading(false)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [flowId, router, router.isReady, aal, refresh, returnTo, flow, onError]) + + useEffect(() => { + if (flow) { + values.setFromFlow(flow) + } + }, [values, flow]) + + const onSubmit = useCallback( + (override?: Partial) => { + setSubmitting(true) + + const body = { + ...(values.getValues() as UpdateVerificationFlowBody), + ...(override || {}), + } + + kratosClient + .updateVerificationFlow( + // @ts-ignore + { flow: String(flow?.id), updateVerificationFlowBody: body }, + { + withCredentials: true, + } + ) + .then(({ data }) => { + setFlow(data) + }) + .catch((error: AxiosError) => { + switch (error.response?.status) { + case 400: + setFlow(error.response?.data) + return + } + + throw error + }) + .finally(() => setSubmitting(false)) + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [flow, values, setSubmitting] + ) + + return ( + + + {/* @ts-ignore Enum conflict with string */} + {children} + + + ) +} diff --git a/packages/identity-integration/src/index.ts b/packages/identity-integration/src/index.ts index d33f98ed..14a57feb 100644 --- a/packages/identity-integration/src/index.ts +++ b/packages/identity-integration/src/index.ts @@ -1,7 +1,6 @@ export { Session } from '@ory/kratos-client' export { Configuration } from '@ory/kratos-client' export * from './providers/index.js' -export * from './flows/index.js' export * from './ui/index.js' export * from './messages/index.js' export * from './sdk/index.js' diff --git a/packages/identity-integration/src/page-router.ts b/packages/identity-integration/src/page-router.ts new file mode 100644 index 00000000..66191f3a --- /dev/null +++ b/packages/identity-integration/src/page-router.ts @@ -0,0 +1 @@ +export * from './flows-page-router/index.js' diff --git a/packages/identity-integration/src/providers/error.context.ts b/packages/identity-integration/src/providers/error.context.ts index 70bf26e9..069cf39a 100644 --- a/packages/identity-integration/src/providers/error.context.ts +++ b/packages/identity-integration/src/providers/error.context.ts @@ -1,5 +1,6 @@ -import type { FlowError } from '@ory/kratos-client' -import { createContext } from 'react' +import type { FlowError } from '@ory/kratos-client' + +import { createContext } from 'react' export interface ContextError { error?: FlowError diff --git a/yarn.lock b/yarn.lock index c1630014..36f5f5b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -188,7 +188,7 @@ __metadata: "@types/react": "npm:18.2.40" "@types/react-dom": "npm:18.2.10" "@types/tldjs": "npm:2.3.4" - axios: "npm:1.5.1" + axios: "npm:1.7.7" next: "npm:14.2.9" react: "npm:18.3.1" react-dom: "npm:18.3.1" @@ -2639,18 +2639,7 @@ __metadata: languageName: node linkType: hard -"axios@npm:1.5.1": - version: 1.5.1 - resolution: "axios@npm:1.5.1" - dependencies: - follow-redirects: "npm:^1.15.0" - form-data: "npm:^4.0.0" - proxy-from-env: "npm:^1.1.0" - checksum: 10/67633db5867c789a6edb6e5229884501bef89584a6718220c243fd5a64de4ea7dcdfdf4f8368a672d582db78aaa9f8d7b619d39403b669f451e1242bbd4c7ee2 - languageName: node - linkType: hard - -"axios@npm:^1.6.1": +"axios@npm:1.7.7, axios@npm:^1.6.1": version: 1.7.7 resolution: "axios@npm:1.7.7" dependencies: @@ -4154,7 +4143,7 @@ __metadata: languageName: node linkType: hard -"follow-redirects@npm:^1.15.0, follow-redirects@npm:^1.15.6": +"follow-redirects@npm:^1.15.6": version: 1.15.9 resolution: "follow-redirects@npm:1.15.9" peerDependenciesMeta: