next-ory/authentication/src/ory/hooks.tsx
2024-05-03 05:50:03 +02:00

199 lines
7.7 KiB
TypeScript

'use client';
import { AxiosError } from 'axios';
import React, { DependencyList, useEffect, useState } from 'react';
import { kratos } from './sdk/kratos';
import { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime';
export const HandleError = (
getFlow:
| ((flowId: string) => Promise<void | AxiosError>)
| undefined = undefined,
setFlow: React.Dispatch<React.SetStateAction<any>> | undefined = undefined,
defaultNav: string | undefined = undefined,
fatalToError = false,
router: AppRouterInstance,
) => {
return async (
error: AxiosError<any, unknown>,
): Promise<AxiosError | void> => {
if (!error.response || error.response?.status === 0) {
window.location.href = `/flow/error?error=${encodeURIComponent(
JSON.stringify(error.response),
)}`;
return Promise.resolve();
}
const responseData = error.response?.data || {};
switch (error.response?.status) {
case 400: {
if (responseData.error?.id == 'session_already_available') {
router.push('/');
return Promise.resolve();
}
// the request could contain invalid parameters which would set error messages in the flow
if (setFlow !== undefined) {
console.warn('sdkError 400: update flow data');
setFlow(responseData);
return Promise.resolve();
}
break;
}
// we have no session or the session is invalid
case 401: {
console.warn('handleError hook 401: Navigate to /login');
router.push('/flow/login');
return Promise.resolve();
}
case 403: {
// the user might have a session, but would require 2FA (Two-Factor Authentication)
if (responseData.error?.id === 'session_aal2_required') {
router.push('/flow/login?aal2=true');
router.refresh();
return Promise.resolve();
}
if (
responseData.error?.id === 'session_refresh_required' &&
responseData.redirect_browser_to
) {
console.warn(
'sdkError 403: Redirect browser to',
responseData.redirect_browser_to,
);
window.location = responseData.redirect_browser_to;
return Promise.resolve();
}
break;
}
case 404: {
console.warn('sdkError 404: Navigate to Error');
const errorMsg = {
data: error.response?.data || error,
status: error.response?.status,
statusText: error.response?.statusText,
url: window.location.href,
};
router.push(
`/flow/error?error=${encodeURIComponent(JSON.stringify(errorMsg))}`,
);
return Promise.resolve();
}
// error.id handling
// "self_service_flow_expired"
case 410: {
if (getFlow !== undefined && responseData.use_flow_id !== undefined) {
console.warn('sdkError 410: Update flow');
return getFlow(responseData.use_flow_id).catch((error) => {
// Something went seriously wrong - log and redirect to defaultNav if possible
console.error(error);
if (defaultNav !== undefined) {
router.push(defaultNav);
} else {
// Rethrow error when can't navigate and let caller handle
throw error;
}
});
} else if (defaultNav !== undefined) {
console.warn('sdkError 410: Navigate to', defaultNav);
router.push(defaultNav);
return Promise.resolve();
}
break;
}
// we need to parse the response and follow the `redirect_browser_to` URL
// this could be when the user needs to perform a 2FA challenge
// or passwordless login
case 422: {
if (responseData.redirect_browser_to !== undefined) {
const currentUrl = new URL(window.location.href);
const redirect = new URL(responseData.redirect_browser_to);
// host name has changed, then change location
if (currentUrl.host !== redirect.host) {
console.warn('sdkError 422: Host changed redirect');
window.location = responseData.redirect_browser_to;
return Promise.resolve();
}
// Path has changed
if (currentUrl.pathname !== redirect.pathname) {
console.warn('sdkError 422: Update path');
router.push(redirect.pathname + redirect.search);
return Promise.resolve();
}
// for webauthn we need to reload the flow
const flowId = redirect.searchParams.get('flow');
if (flowId != null && getFlow !== undefined) {
// get new flow data based on the flow id in the redirect url
console.warn('sdkError 422: Update flow');
return getFlow(flowId).catch((error) => {
// Something went seriously wrong - log and redirect to defaultNav if possible
console.error(error);
if (defaultNav !== undefined) {
router.push(defaultNav);
} else {
// Rethrow error when can't navigate and let caller handle
throw error;
}
});
} else {
console.warn('sdkError 422: Redirect browser to');
window.location = responseData.redirect_browser_to;
return Promise.resolve();
}
}
}
}
console.error(error);
if (fatalToError) {
console.warn('sdkError: fatal error redirect to /error');
router.push('/flow/error?id=' + encodeURI(error.response?.data.error?.id));
return Promise.resolve();
}
throw error;
};
};
// Returns a function which will log the user out
export function LogoutLink(deps?: DependencyList) {
const [logoutToken, setLogoutToken] = useState<string>('');
useEffect(() => {
kratos
.createBrowserLogoutFlow()
.then(({ data }) => {
setLogoutToken(data.logout_token);
})
.catch((err: AxiosError) => {
switch (err.response?.status) {
case 401:
// do nothing, the user is not logged in
return;
}
// Something else happened!
return Promise.reject(err);
});
}, deps);
return () => {
if (logoutToken) {
kratos
.updateLogoutFlow({ token: logoutToken })
.then(() => window.location.href = '/flow/login');
}
};
}