diff --git a/packages/identity-integration/README.md b/packages/identity-integration/README.md index f1244b79..c328e6ec 100644 --- a/packages/identity-integration/README.md +++ b/packages/identity-integration/README.md @@ -1,5 +1,13 @@ # NextJS Custom Workshop +## BREAKING CHANGE 0.0.16 + +- Необходим глобальный провайдер `KratosClientProvider`. По умолчанию он предоставляет + стандартный клиент `kratos`, однако его можно будет изменять/расширять по потребности в + проекте, а также URL редиректа для некоторых ошибок. +- Добавлена кастомизация URL для редиректа в случае ошибок, например пользователь уже авторизован. +- Расширен конструктов класса `KratosClient` + ## BREAKING CHANGE 0.0.14 - Обновлены пакеты `ory/kratos-client`, `ory/integrations`, `nextjs` diff --git a/packages/identity-integration/src/flows/error.flow.tsx b/packages/identity-integration/src/flows/error.flow.tsx index 8b4c2a01..ee264341 100644 --- a/packages/identity-integration/src/flows/error.flow.tsx +++ b/packages/identity-integration/src/flows/error.flow.tsx @@ -9,7 +9,7 @@ import { useState } from 'react' import { useEffect } from 'react' import { ErrorProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' export interface ErrorErrorProps { returnToUrl?: string @@ -20,6 +20,8 @@ export const ErrorFlow: FC> = ({ children, re const [loading, setLoading] = useState(true) const router = useRouter() + const { kratosClient } = useKratosClient() + const { id } = router.query useEffect(() => { @@ -27,7 +29,7 @@ export const ErrorFlow: FC> = ({ children, re return } - kratos + kratosClient .getFlowError({ id: String(id) }) .then(({ data }) => { setError(data) diff --git a/packages/identity-integration/src/flows/handle-errors.util.ts b/packages/identity-integration/src/flows/handle-errors.util.ts index 122f54d7..c8a25a00 100644 --- a/packages/identity-integration/src/flows/handle-errors.util.ts +++ b/packages/identity-integration/src/flows/handle-errors.util.ts @@ -11,9 +11,12 @@ 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 @@ -23,7 +26,7 @@ export const handleFlowError = ( if (error.response?.data?.redirect_browser_to) { window.location.href = error.response.data.redirect_browser_to } else { - await router.push('/profile/settings') + await router.push(redirectToSettings) } return @@ -38,7 +41,7 @@ export const handleFlowError = ( onResetFlow(undefined) - await router.push(flowType === 'settings' ? '/profile/settings' : '/auth/' + flowType) + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) return case 'self_service_flow_expired': @@ -48,7 +51,7 @@ export const handleFlowError = ( onResetFlow(undefined) - await router.push(flowType === 'settings' ? '/profile/settings' : '/auth/' + flowType) + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) return case 'security_csrf_violation': @@ -58,13 +61,13 @@ export const handleFlowError = ( onResetFlow(undefined) - await router.push(flowType === 'settings' ? '/profile/settings' : '/auth/' + flowType) + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) return case 'security_identity_mismatch': onResetFlow(undefined) - await router.push(flowType === 'settings' ? '/profile/settings' : '/auth/' + flowType) + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) return case 'browser_location_change_required': @@ -77,7 +80,7 @@ export const handleFlowError = ( case 410: onResetFlow(undefined) - await router.push(flowType === 'settings' ? '/profile/settings' : '/auth/' + flowType) + await router.push(flowType === 'settings' ? redirectToSettings : '/auth/' + flowType) return } diff --git a/packages/identity-integration/src/flows/login.flow.tsx b/packages/identity-integration/src/flows/login.flow.tsx index 349eeda0..b9c66e0b 100644 --- a/packages/identity-integration/src/flows/login.flow.tsx +++ b/packages/identity-integration/src/flows/login.flow.tsx @@ -15,7 +15,7 @@ import { FlowProvider } from '../providers' import { ValuesProvider } from '../providers' import { ValuesStore } from '../providers' import { SubmitProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' import { handleFlowError } from './handle-errors.util' export interface LoginFlowProps { @@ -33,6 +33,7 @@ export const LoginFlow: FC> = ({ 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 @@ -42,18 +43,18 @@ export const LoginFlow: FC> = ({ } if (flowId) { - kratos + kratosClient .getLoginFlow({ id: String(flowId) }, { withCredentials: true }) .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'login', setFlow, onError)) + .catch(handleFlowError(router, 'login', setFlow, returnToSettingsUrl, onError)) .finally(() => setLoading(false)) return } - kratos + kratosClient .createBrowserLoginFlow( { refresh: Boolean(refresh), @@ -65,7 +66,7 @@ export const LoginFlow: FC> = ({ .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'login', setFlow, onError)) + .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]) @@ -85,7 +86,7 @@ export const LoginFlow: FC> = ({ ...(override || {}), } - kratos + kratosClient .updateLoginFlow( { flow: String(flow?.id), updateLoginFlowBody: body }, { withCredentials: true } @@ -97,7 +98,7 @@ export const LoginFlow: FC> = ({ router.push(returnToUrl ?? '/') } }) - .catch(handleFlowError(router, 'login', setFlow)) + .catch(handleFlowError(router, 'login', setFlow, returnToSettingsUrl)) .catch((error: AxiosError) => { if (error.response?.status === 400) { setFlow(error.response?.data) diff --git a/packages/identity-integration/src/flows/logout.flow.tsx b/packages/identity-integration/src/flows/logout.flow.tsx index bdaa4646..5383dcfe 100644 --- a/packages/identity-integration/src/flows/logout.flow.tsx +++ b/packages/identity-integration/src/flows/logout.flow.tsx @@ -8,7 +8,7 @@ import { useRouter } from 'next/router' import { useState } from 'react' import { useEffect } from 'react' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' interface LogoutFlowProps { returnToUrl?: string @@ -17,6 +17,7 @@ interface LogoutFlowProps { export const LogoutFlow: FC> = ({ children, returnToUrl }) => { const [logoutToken, setLogoutToken] = useState('') const router = useRouter() + const { kratosClient } = useKratosClient() const { return_to: returnTo } = router.query @@ -25,7 +26,7 @@ export const LogoutFlow: FC> = ({ children, r return } - kratos + kratosClient .createBrowserLogoutFlow( { returnTo: returnTo?.toString() ?? returnToUrl }, { withCredentials: true } @@ -47,10 +48,11 @@ export const LogoutFlow: FC> = ({ children, r useEffect(() => { if (logoutToken) { - kratos + 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 diff --git a/packages/identity-integration/src/flows/recovery.flow.tsx b/packages/identity-integration/src/flows/recovery.flow.tsx index ed17505c..c96f82e1 100644 --- a/packages/identity-integration/src/flows/recovery.flow.tsx +++ b/packages/identity-integration/src/flows/recovery.flow.tsx @@ -15,7 +15,7 @@ import { FlowProvider } from '../providers' import { ValuesProvider } from '../providers' import { ValuesStore } from '../providers' import { SubmitProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' import { handleFlowError } from './handle-errors.util' export interface RecoveryFlowProps { @@ -33,6 +33,7 @@ export const RecoveryFlow: FC> = ({ 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 @@ -42,18 +43,18 @@ export const RecoveryFlow: FC> = ({ } if (flowId) { - kratos + kratosClient .getRecoveryFlow({ id: String(flowId) }, { withCredentials: true }) .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'recovery', setFlow, onError)) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl, onError)) .finally(() => setLoading(false)) return } - kratos + kratosClient .createBrowserRecoveryFlow( { returnTo: returnTo?.toString() ?? returnToUrl }, { @@ -63,7 +64,7 @@ export const RecoveryFlow: FC> = ({ .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'recovery', setFlow, onError)) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl, onError)) .catch((error: AxiosError) => { if (error.response?.status === 400) { setFlow(error.response?.data) @@ -93,7 +94,7 @@ export const RecoveryFlow: FC> = ({ ...(override || {}), } - kratos + kratosClient .updateRecoveryFlow( { flow: String(flow?.id), updateRecoveryFlowBody: body }, { withCredentials: true } @@ -101,7 +102,7 @@ export const RecoveryFlow: FC> = ({ .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'recovery', setFlow)) + .catch(handleFlowError(router, 'recovery', setFlow, returnToSettingsUrl)) .catch((error: AxiosError) => { if (error.response?.status === 400) { setFlow(error.response?.data) @@ -114,6 +115,7 @@ export const RecoveryFlow: FC> = ({ }) .finally(() => setSubmitting(false)) }, + // eslint-disable-next-line react-hooks/exhaustive-deps [router, flow, values, setSubmitting] ) diff --git a/packages/identity-integration/src/flows/registration.flow.tsx b/packages/identity-integration/src/flows/registration.flow.tsx index e3938b7e..930297d6 100644 --- a/packages/identity-integration/src/flows/registration.flow.tsx +++ b/packages/identity-integration/src/flows/registration.flow.tsx @@ -17,7 +17,7 @@ import { FlowProvider } from '../providers' import { ValuesProvider } from '../providers' import { ValuesStore } from '../providers' import { SubmitProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' import { handleFlowError } from './handle-errors.util' export interface RegistrationFlowProps { @@ -37,6 +37,7 @@ export const RegistrationFlow: FC> = ({ 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 @@ -46,18 +47,18 @@ export const RegistrationFlow: FC> = ({ } if (flowId) { - kratos + kratosClient .getRegistrationFlow({ id: String(flowId) }, { withCredentials: true }) .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'registration', setFlow, onError)) + .catch(handleFlowError(router, 'registration', setFlow, returnToSettingsUrl, onError)) .finally(() => setLoading(false)) return } - kratos + kratosClient .createBrowserRegistrationFlow( { returnTo: returnTo?.toString() ?? returnToUrl }, { @@ -67,7 +68,7 @@ export const RegistrationFlow: FC> = ({ .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'registration', setFlow, onError)) + .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]) @@ -104,7 +105,7 @@ export const RegistrationFlow: FC> = ({ ...(override || {}), } - kratos + kratosClient .updateRegistrationFlow( { flow: String(flow?.id), updateRegistrationFlowBody: body }, { withCredentials: true } @@ -119,7 +120,7 @@ export const RegistrationFlow: FC> = ({ router.push(returnToUrl ?? '/') } }) - .catch(handleFlowError(router, 'registration', setFlow)) + .catch(handleFlowError(router, 'registration', setFlow, returnToSettingsUrl)) .catch((error: AxiosError) => { if (error.response?.status === 400) { setFlow(error.response?.data) diff --git a/packages/identity-integration/src/flows/settings.flow.tsx b/packages/identity-integration/src/flows/settings.flow.tsx index c2054cc5..8cebcf0c 100644 --- a/packages/identity-integration/src/flows/settings.flow.tsx +++ b/packages/identity-integration/src/flows/settings.flow.tsx @@ -15,7 +15,7 @@ import { FlowProvider } from '../providers' import { ValuesProvider } from '../providers' import { ValuesStore } from '../providers' import { SubmitProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' import { handleFlowError } from './handle-errors.util' export interface SettingsFlowProps { @@ -33,6 +33,7 @@ export const SettingsFlow: FC> = ({ 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 @@ -42,18 +43,18 @@ export const SettingsFlow: FC> = ({ } if (flowId) { - kratos + kratosClient .getSettingsFlow({ id: String(flowId) }, { withCredentials: true }) .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'settings', setFlow, onError)) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl, onError)) .finally(() => setLoading(false)) return } - kratos + kratosClient .createBrowserSettingsFlow( { returnTo: returnTo?.toString() ?? returnToUrl }, { @@ -63,7 +64,7 @@ export const SettingsFlow: FC> = ({ .then(({ data }) => { setFlow(data) }) - .catch(handleFlowError(router, 'settings', setFlow, onError)) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl, onError)) .catch((error: AxiosError) => { // eslint-disable-next-line default-case switch (error.response?.status) { @@ -96,7 +97,7 @@ export const SettingsFlow: FC> = ({ ...(override || {}), } - kratos + kratosClient .updateSettingsFlow( { flow: String(flow?.id), updateSettingsFlowBody: body }, { withCredentials: true } @@ -105,7 +106,7 @@ export const SettingsFlow: FC> = ({ setFlow(data) router.push('/') }) - .catch(handleFlowError(router, 'settings', setFlow)) + .catch(handleFlowError(router, 'settings', setFlow, returnToSettingsUrl)) .catch((error: AxiosError) => { if (error.response?.status === 400) { setFlow(error.response?.data) @@ -118,6 +119,7 @@ export const SettingsFlow: FC> = ({ }) .finally(() => setSubmitting(false)) }, + // eslint-disable-next-line react-hooks/exhaustive-deps [router, flow, values, setSubmitting] ) diff --git a/packages/identity-integration/src/flows/verification.flow.tsx b/packages/identity-integration/src/flows/verification.flow.tsx index f178cf3e..bab01a2c 100644 --- a/packages/identity-integration/src/flows/verification.flow.tsx +++ b/packages/identity-integration/src/flows/verification.flow.tsx @@ -17,7 +17,7 @@ import { FlowProvider } from '../providers' import { ValuesProvider } from '../providers' import { ValuesStore } from '../providers' import { SubmitProvider } from '../providers' -import { kratos } from '../sdk' +import { useKratosClient } from '../providers' export interface VerificationFlowProps { onError?: (error: { id: string }) => void @@ -34,6 +34,7 @@ export const VerificationFlow: FC> = ({ 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 @@ -43,7 +44,7 @@ export const VerificationFlow: FC> = ({ } if (flowId) { - kratos + kratosClient .getVerificationFlow({ id: String(flowId) }, { withCredentials: true }) .then(({ data }) => { setFlow(data) @@ -62,7 +63,7 @@ export const VerificationFlow: FC> = ({ return } - kratos + kratosClient .createBrowserVerificationFlow( { returnTo: returnTo?.toString() ?? returnToUrl }, { @@ -99,7 +100,7 @@ export const VerificationFlow: FC> = ({ ...(override || {}), } - kratos + kratosClient .updateVerificationFlow( { flow: String(flow?.id), updateVerificationFlowBody: body }, { @@ -120,6 +121,7 @@ export const VerificationFlow: FC> = ({ }) .finally(() => setSubmitting(false)) }, + // eslint-disable-next-line react-hooks/exhaustive-deps [flow, values, setSubmitting] ) diff --git a/packages/identity-integration/src/index.ts b/packages/identity-integration/src/index.ts index 62c38172..cecc2039 100644 --- a/packages/identity-integration/src/index.ts +++ b/packages/identity-integration/src/index.ts @@ -2,3 +2,4 @@ export * from './providers' export * from './flows' export * from './ui' export * from './messages' +export * from './sdk' diff --git a/packages/identity-integration/src/providers/index.ts b/packages/identity-integration/src/providers/index.ts index 7a19bc20..abcaaa30 100644 --- a/packages/identity-integration/src/providers/index.ts +++ b/packages/identity-integration/src/providers/index.ts @@ -10,3 +10,5 @@ export * from './submit.context' export * from './use-submit.hook' export * from './error.context' export * from './use-error.hook' +export * from './kratos-client.context' +export * from './use-kratos-client.hook' diff --git a/packages/identity-integration/src/providers/kratos-client.context.ts b/packages/identity-integration/src/providers/kratos-client.context.ts new file mode 100644 index 00000000..eced5972 --- /dev/null +++ b/packages/identity-integration/src/providers/kratos-client.context.ts @@ -0,0 +1,20 @@ +import { createContext } from 'react' + +import { KratosClient } from '../sdk' +import { kratos } from '../sdk' + +export interface ContextKratosClient { + kratosClient?: KratosClient + returnToSettingsUrl?: string +} + +const Context = createContext({ + kratosClient: kratos, + returnToSettingsUrl: '/profile/settings', +}) + +const { Provider, Consumer } = Context + +export const KratosClientProvider = Provider +export const KratosClientConsumer = Consumer +export const KratosClientContext = Context diff --git a/packages/identity-integration/src/providers/use-kratos-client.hook.ts b/packages/identity-integration/src/providers/use-kratos-client.hook.ts new file mode 100644 index 00000000..f0f5b21f --- /dev/null +++ b/packages/identity-integration/src/providers/use-kratos-client.hook.ts @@ -0,0 +1,18 @@ +import { useContext } from 'react' + +import { ContextKratosClient } from './kratos-client.context' +import { KratosClientContext } from './kratos-client.context' + +export const useKratosClient = (): Required => { + const { kratosClient, returnToSettingsUrl } = useContext(KratosClientContext) + + if (!kratosClient) { + throw new Error('Missing ') + } + + if (!returnToSettingsUrl) { + throw new Error('Missing returnToSettingsUrl') + } + + return { kratosClient, returnToSettingsUrl } +} diff --git a/packages/identity-integration/src/sdk/index.ts b/packages/identity-integration/src/sdk/index.ts index dc3f8f21..e6967c6e 100644 --- a/packages/identity-integration/src/sdk/index.ts +++ b/packages/identity-integration/src/sdk/index.ts @@ -4,7 +4,7 @@ import { FrontendApi } from '@ory/kratos-client' import { getDomain } from 'tldjs' export class KratosClient extends FrontendApi { - constructor(basePath?: string) { + constructor({ basePath, ...props }: Partial) { if (!basePath && typeof window !== 'undefined') { const { hostname, protocol } = window.location @@ -19,12 +19,8 @@ export class KratosClient extends FrontendApi { } } - super( - new Configuration({ - basePath, - }) - ) + super(new Configuration({ basePath, ...props })) } } -export const kratos = new KratosClient() +export const kratos = new KratosClient({})