diff --git a/packages/cloud/src/middleware/with-security-headers.ts b/packages/cloud/src/middleware/with-security-headers.ts index 500155d8d..a6b92a549 100644 --- a/packages/cloud/src/middleware/with-security-headers.ts +++ b/packages/cloud/src/middleware/with-security-headers.ts @@ -96,7 +96,12 @@ export default function withSecurityHeaders 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, diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index 76112a50f..f74ba4fee 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -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() { - {/** - * 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 ? ( - - - - - - ) : ( - - )} + + {/** + * 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 ? ( + + + + + + ) : ( + + )} + diff --git a/packages/console/src/cloud/App.tsx b/packages/console/src/cloud/App.tsx index a53d83464..11c85cffc 100644 --- a/packages/console/src/cloud/App.tsx +++ b/packages/console/src/cloud/App.tsx @@ -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'; diff --git a/packages/console/src/cloud/pages/Callback/index.tsx b/packages/console/src/cloud/pages/Callback/index.tsx deleted file mode 100644 index 821c9a620..000000000 --- a/packages/console/src/cloud/pages/Callback/index.tsx +++ /dev/null @@ -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 ( -
- Error Occurred: -
- {error.message} -
- ); - } - - return ; -} - -export default Callback; diff --git a/packages/console/src/containers/AppContent/index.tsx b/packages/console/src/containers/AppContent/index.tsx index ede7e02e4..68f4ef68f 100644 --- a/packages/console/src/containers/AppContent/index.tsx +++ b/packages/console/src/containers/AppContent/index.tsx @@ -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(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 ; } - if (error) { - if (error instanceof LogtoClientError) { - return ; - } - - if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') { - return ; - } - - return ; - } - return ( <>
diff --git a/packages/console/src/containers/ErrorBoundary/index.tsx b/packages/console/src/containers/ErrorBoundary/index.tsx index a053ddd52..81fc8bc72 100644 --- a/packages/console/src/containers/ErrorBoundary/index.tsx +++ b/packages/console/src/containers/ErrorBoundary/index.tsx @@ -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 { } 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 ? : 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 ( + + ); + } + + return ( + + ); + } + + // Insecure contexts error is not recoverable + if (error instanceof LogtoError && error.code === 'crypto_subtle_unavailable') { + return ; + } + + // 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 ; } const callStack = conditional( @@ -69,4 +104,4 @@ class ErrorBoundary extends Component { } } -export default ErrorBoundary; +export default withTranslation('translation', { keyPrefix: 'admin_console' })(ErrorBoundary); diff --git a/packages/console/src/containers/LogtoErrorBoundary/index.tsx b/packages/console/src/containers/LogtoErrorBoundary/index.tsx new file mode 100644 index 000000000..d93cbcc11 --- /dev/null +++ b/packages/console/src/containers/LogtoErrorBoundary/index.tsx @@ -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 ``. + */ +export default function LogtoErrorBoundary({ children }: { children: JSX.Element }) { + const { error } = useLogto(); + + useEffect(() => { + if (error) { + throw error; + } + }, [error]); + + return children; +} diff --git a/packages/console/src/pages/Callback/index.tsx b/packages/console/src/pages/Callback/index.tsx index 4d877c9e5..1165f7d77 100644 --- a/packages/console/src/pages/Callback/index.tsx +++ b/packages/console/src/pages/Callback/index.tsx @@ -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 ; - } - return ; } diff --git a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx index 3febf319f..a92f7aad3 100644 --- a/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx +++ b/packages/console/src/pages/TenantSettings/TenantBasicSettings/index.tsx @@ -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(); const [isDeletionModalOpen, setIsDeletionModalOpen] = useState(false); const [isDeleting, setIsDeleting] = useState(false); diff --git a/packages/console/src/pages/Welcome/index.tsx b/packages/console/src/pages/Welcome/index.tsx index 9556d22a4..bd2833ff3 100644 --- a/packages/console/src/pages/Welcome/index.tsx +++ b/packages/console/src/pages/Welcome/index.tsx @@ -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 ; - } - - return ; - } - return (
diff --git a/packages/core/src/middleware/koa-security-headers.ts b/packages/core/src/middleware/koa-security-headers.ts index 71f48b444..03c95b754 100644 --- a/packages/core/src/middleware/koa-security-headers.ts +++ b/packages/core/src/middleware/koa-security-headers.ts @@ -77,7 +77,11 @@ export default function koaSecurityHeaders( 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( 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],