mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
refactor(console): reorg error rendering (#4082)
* refactor(console): reorg error rendering * refactor(console): reorg error rendering
This commit is contained in:
parent
850cd3e342
commit
ad6dde7f18
11 changed files with 99 additions and 96 deletions
|
@ -96,7 +96,12 @@ export default function withSecurityHeaders<InputContext extends RequestContext>
|
|||
directives: {
|
||||
'upgrade-insecure-requests': null,
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
scriptSrc: ["'self'", ...gtagOrigins],
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
...gtagOrigins,
|
||||
// Non-production environment allow "unsafe-eval" and "unsafe-inline" for debugging purpose
|
||||
...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]),
|
||||
],
|
||||
connectSrc: [
|
||||
"'self'",
|
||||
...adminOrigins,
|
||||
|
|
|
@ -19,6 +19,7 @@ import { cloudApi, getManagementApi, meApi } from '@/consts/resources';
|
|||
import { adminTenantEndpoint, mainTitle } from './consts';
|
||||
import { isCloud } from './consts/env';
|
||||
import ErrorBoundary from './containers/ErrorBoundary';
|
||||
import LogtoErrorBoundary from './containers/LogtoErrorBoundary';
|
||||
import TenantAppContainer from './containers/TenantAppContainer';
|
||||
import AppConfirmModalProvider from './contexts/AppConfirmModalProvider';
|
||||
import AppEndpointsProvider from './contexts/AppEndpointsProvider';
|
||||
|
@ -76,19 +77,21 @@ function Content() {
|
|||
<AppInsightsBoundary cloudRole="console">
|
||||
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
|
||||
<ErrorBoundary>
|
||||
{/**
|
||||
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
|
||||
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
|
||||
*/}
|
||||
{!isCloud || currentTenantId ? (
|
||||
<AppEndpointsProvider>
|
||||
<AppConfirmModalProvider>
|
||||
<TenantAppContainer />
|
||||
</AppConfirmModalProvider>
|
||||
</AppEndpointsProvider>
|
||||
) : (
|
||||
<CloudApp />
|
||||
)}
|
||||
<LogtoErrorBoundary>
|
||||
{/**
|
||||
* If it's not Cloud (OSS), render the tenant app container directly since only default tenant is available;
|
||||
* if it's Cloud, render the tenant app container only when a tenant ID is available (in a tenant context).
|
||||
*/}
|
||||
{!isCloud || currentTenantId ? (
|
||||
<AppEndpointsProvider>
|
||||
<AppConfirmModalProvider>
|
||||
<TenantAppContainer />
|
||||
</AppConfirmModalProvider>
|
||||
</AppEndpointsProvider>
|
||||
) : (
|
||||
<CloudApp />
|
||||
)}
|
||||
</LogtoErrorBoundary>
|
||||
</ErrorBoundary>
|
||||
</AppInsightsBoundary>
|
||||
</AppThemeProvider>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { BrowserRouter, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import ProtectedRoutes from '@/containers/ProtectedRoutes';
|
||||
import Callback from '@cloud/pages/Callback';
|
||||
import Callback from '@/pages/Callback';
|
||||
|
||||
import * as styles from './App.module.scss';
|
||||
import Main from './pages/Main';
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { useHandleSignInCallback } from '@logto/react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import { getUserTenantId } from '@/consts/tenants';
|
||||
|
||||
function Callback() {
|
||||
const navigate = useNavigate();
|
||||
const { error } = useHandleSignInCallback(() => {
|
||||
navigate('/' + getUserTenantId(), { replace: true });
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
Error Occurred:
|
||||
<br />
|
||||
{error.message}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <AppLoading />;
|
||||
}
|
||||
|
||||
export default Callback;
|
|
@ -1,12 +1,8 @@
|
|||
import { LogtoClientError, LogtoError, useLogto } from '@logto/react';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Outlet, useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
import AppError from '@/components/AppError';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import SessionExpired from '@/components/SessionExpired';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import useConfigs from '@/hooks/use-configs';
|
||||
import useScroll from '@/hooks/use-scroll';
|
||||
|
@ -21,7 +17,6 @@ import * as styles from './index.module.scss';
|
|||
import { type AppContentOutletContext } from './types';
|
||||
|
||||
function AppContent() {
|
||||
const { error } = useLogto();
|
||||
const { isLoading: isPreferencesLoading } = useUserPreferences();
|
||||
const { isLoading: isConfigsLoading } = useConfigs();
|
||||
|
||||
|
@ -32,7 +27,6 @@ function AppContent() {
|
|||
const { firstItem } = useSidebarMenuItems();
|
||||
const scrollableContent = useRef<HTMLDivElement>(null);
|
||||
const { scrollTop } = useScroll(scrollableContent.current);
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
|
||||
useEffect(() => {
|
||||
// Navigate to the first menu item after configs are loaded.
|
||||
|
@ -45,18 +39,6 @@ function AppContent() {
|
|||
return <AppLoading />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (error instanceof LogtoClientError) {
|
||||
return <SessionExpired error={error} />;
|
||||
}
|
||||
|
||||
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {
|
||||
return <AppError errorMessage={t('errors.insecure_contexts')} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.app}>
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
import { LogtoClientError, LogtoError, OidcError } from '@logto/react';
|
||||
import { conditional } from '@silverhand/essentials';
|
||||
import { ResponseError } from '@withtyped/client';
|
||||
import { type TFunction } from 'i18next';
|
||||
import { HTTPError } from 'ky';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Component } from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
|
||||
import AppError from '@/components/AppError';
|
||||
import SessionExpired from '@/components/SessionExpired';
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
t: TFunction<'translation', 'admin_console'>;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
@ -48,12 +53,42 @@ class ErrorBoundary extends Component<Props, State> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { children } = this.props;
|
||||
const { children, t } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
if (error) {
|
||||
if (error instanceof HTTPError) {
|
||||
return error.response.status === 401 ? <SessionExpired error={error} /> : children;
|
||||
// Different strategies for handling errors in callback pages since the callback errors
|
||||
// are likely unexpected and unrecoverable.
|
||||
const { pathname } = window.location;
|
||||
if (['/callback', '-callback'].some((path) => pathname.endsWith(path))) {
|
||||
if (error instanceof LogtoError && error.data instanceof OidcError) {
|
||||
return (
|
||||
<AppError
|
||||
errorCode={error.data.error}
|
||||
errorMessage={error.data.errorDescription}
|
||||
callStack={error.stack}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AppError errorCode={error.name} errorMessage={error.message} callStack={error.stack} />
|
||||
);
|
||||
}
|
||||
|
||||
// Insecure contexts error is not recoverable
|
||||
if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') {
|
||||
return <AppError errorMessage={t('errors.insecure_contexts')} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
// Treat other Logto errors and 401 responses as session expired
|
||||
if (
|
||||
error instanceof LogtoError ||
|
||||
error instanceof LogtoClientError ||
|
||||
(error instanceof HTTPError && error.response.status === 401) ||
|
||||
(error instanceof ResponseError && error.status === 401)
|
||||
) {
|
||||
return <SessionExpired error={error} />;
|
||||
}
|
||||
|
||||
const callStack = conditional(
|
||||
|
@ -69,4 +104,4 @@ class ErrorBoundary extends Component<Props, State> {
|
|||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
export default withTranslation('translation', { keyPrefix: 'admin_console' })(ErrorBoundary);
|
||||
|
|
18
packages/console/src/containers/LogtoErrorBoundary/index.tsx
Normal file
18
packages/console/src/containers/LogtoErrorBoundary/index.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { useLogto } from '@logto/react';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* This component keep children as is, but throw error if there is any error from Logto
|
||||
* (`useLogto()`). The error should be handled by the upper `<ErrorBoundary />`.
|
||||
*/
|
||||
export default function LogtoErrorBoundary({ children }: { children: JSX.Element }) {
|
||||
const { error } = useLogto();
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}, [error]);
|
||||
|
||||
return children;
|
||||
}
|
|
@ -1,28 +1,17 @@
|
|||
import { LogtoError, OidcError, useHandleSignInCallback } from '@logto/react';
|
||||
import { useHandleSignInCallback } from '@logto/react';
|
||||
import { conditionalString } from '@silverhand/essentials';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import AppError from '@/components/AppError';
|
||||
import AppLoading from '@/components/AppLoading';
|
||||
import { isCloud } from '@/consts/env';
|
||||
import { getUserTenantId } from '@/consts/tenants';
|
||||
|
||||
function Callback() {
|
||||
const navigate = useNavigate();
|
||||
const { error } = useHandleSignInCallback(() => {
|
||||
navigate('/');
|
||||
useHandleSignInCallback(() => {
|
||||
navigate('/' + conditionalString(isCloud && getUserTenantId()), { replace: true });
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const errorCode =
|
||||
error instanceof LogtoError && error.data instanceof OidcError
|
||||
? error.data.error
|
||||
: error.name;
|
||||
const errorMessage =
|
||||
error instanceof LogtoError && error.data instanceof OidcError
|
||||
? error.data.errorDescription
|
||||
: error.message;
|
||||
|
||||
return <AppError errorCode={errorCode} errorMessage={errorMessage} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
return <AppLoading />;
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ const tenantProfileToForm = (tenant?: TenantInfo): TenantSettingsForm => {
|
|||
function TenantBasicSettings() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const api = useCloudApi();
|
||||
const { currentTenant, currentTenantId, tenants, updateTenant, removeTenant } =
|
||||
useContext(TenantsContext);
|
||||
const { currentTenant, currentTenantId, updateTenant, removeTenant } = useContext(TenantsContext);
|
||||
const [error, setError] = useState<Error>();
|
||||
const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
import { LogtoClientError, useLogto } from '@logto/react';
|
||||
import { useLogto } from '@logto/react';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
import Logo from '@/assets/images/logo.svg';
|
||||
import AppError from '@/components/AppError';
|
||||
import SessionExpired from '@/components/SessionExpired';
|
||||
import { getCallbackUrl } from '@/consts';
|
||||
import Button from '@/ds-components/Button';
|
||||
import useTheme from '@/hooks/use-theme';
|
||||
|
@ -16,7 +14,7 @@ import * as styles from './index.module.scss';
|
|||
function Welcome() {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const navigate = useNavigate();
|
||||
const { isAuthenticated, error, signIn } = useLogto();
|
||||
const { isAuthenticated, signIn } = useLogto();
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -26,14 +24,6 @@ function Welcome() {
|
|||
}
|
||||
}, [isAuthenticated, navigate]);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof LogtoClientError) {
|
||||
return <SessionExpired error={error} />;
|
||||
}
|
||||
|
||||
return <AppError errorMessage={error.message} callStack={error.stack} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames(styles.container, styles[theme])}>
|
||||
<div className={styles.header}>
|
||||
|
|
|
@ -77,7 +77,11 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
|
|||
directives: {
|
||||
'upgrade-insecure-requests': null,
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
scriptSrc: ["'self'"],
|
||||
// Non-production environment allow "unsafe-eval" and "unsafe-inline" for debugging purpose
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]),
|
||||
],
|
||||
connectSrc: ["'self'", tenantEndpointOrigin, ...developmentOrigins, ...appInsightsOrigins],
|
||||
// WARNING: high risk Need to allow self hosted terms of use page loaded in an iframe
|
||||
frameSrc: ["'self'", 'https:'],
|
||||
|
@ -97,7 +101,11 @@ export default function koaSecurityHeaders<StateT, ContextT, ResponseBodyT>(
|
|||
directives: {
|
||||
'upgrade-insecure-requests': null,
|
||||
imgSrc: ["'self'", 'data:', 'https:'],
|
||||
scriptSrc: ["'self'"],
|
||||
// Non-production environment allow "unsafe-eval" and "unsafe-inline" for debugging purpose
|
||||
scriptSrc: [
|
||||
"'self'",
|
||||
...conditionalArray(!isProduction && ["'unsafe-eval'", "'unsafe-inline'"]),
|
||||
],
|
||||
connectSrc: ["'self'", ...adminOrigins, ...coreOrigins, ...developmentOrigins],
|
||||
// Allow Main Flow origin loaded in preview iframe
|
||||
frameSrc: ["'self'", ...adminOrigins, ...coreOrigins],
|
||||
|
|
Loading…
Add table
Reference in a new issue