diff --git a/packages/next/src/build/webpack/loaders/next-app-loader.ts b/packages/next/src/build/webpack/loaders/next-app-loader.ts index 3fdf6b4633384..db6e4111dd8b4 100644 --- a/packages/next/src/build/webpack/loaders/next-app-loader.ts +++ b/packages/next/src/build/webpack/loaders/next-app-loader.ts @@ -58,6 +58,7 @@ type AppLoader = webpack.LoaderDefinitionFunction const UI_FILE_TYPES = { 'not-found': 'not-found', forbidden: 'forbidden', + unauthorized: 'unauthorized', } as const const FILE_TYPES = { @@ -75,6 +76,7 @@ const PARALLEL_CHILDREN_SEGMENT = 'children$' const defaultUIErrorPaths: { [k in keyof typeof UI_FILE_TYPES]: string } = { 'not-found': 'next/dist/client/components/not-found-error', forbidden: 'next/dist/client/components/forbidden-error', + unauthorized: 'next/dist/client/components/unauthorized-error', } const defaultGlobalErrorPath = 'next/dist/client/components/error-boundary' diff --git a/packages/next/src/client/components/dev-root-not-found-boundary.tsx b/packages/next/src/client/components/dev-root-not-found-boundary.tsx index 4ef0588560ef2..22442875eac21 100644 --- a/packages/next/src/client/components/dev-root-not-found-boundary.tsx +++ b/packages/next/src/client/components/dev-root-not-found-boundary.tsx @@ -1,7 +1,11 @@ 'use client' import React from 'react' -import { ForbiddenBoundary, NotFoundBoundary } from './ui-errors-boundaries' +import { + ForbiddenBoundary, + NotFoundBoundary, + UnauthorizedBoundary, +} from './ui-errors-boundaries' import type { UIErrorHelper } from '../../shared/lib/ui-error-types' export function bailOnUIError(uiError: UIErrorHelper) { @@ -18,16 +22,23 @@ function NotAllowedRootForbiddenError() { return null } +function NotAllowedRootUnauthorizedError() { + bailOnUIError('unauthorized') + return null +} + export function DevRootUIErrorsBoundary({ children, }: { children: React.ReactNode }) { return ( - }> - }> - {children} - - + }> + }> + }> + {children} + + + ) } diff --git a/packages/next/src/client/components/is-next-router-error.ts b/packages/next/src/client/components/is-next-router-error.ts index 865d24b8cb63b..c5cac84cdcccb 100644 --- a/packages/next/src/client/components/is-next-router-error.ts +++ b/packages/next/src/client/components/is-next-router-error.ts @@ -1,6 +1,7 @@ import { isNotFoundError } from './not-found' import { isRedirectError } from './redirect' import { isForbiddenError } from './forbidden' +import { isUnauthorizedError } from './unauthorized' export function isNextRouterError(error: any): boolean { return ( @@ -8,6 +9,7 @@ export function isNextRouterError(error: any): boolean { error.digest && (isRedirectError(error) || isNotFoundError(error) || - isForbiddenError(error)) + isForbiddenError(error) || + isUnauthorizedError(error)) ) } diff --git a/packages/next/src/client/components/layout-router.tsx b/packages/next/src/client/components/layout-router.tsx index 7b17e3c313ec5..900e622a22b92 100644 --- a/packages/next/src/client/components/layout-router.tsx +++ b/packages/next/src/client/components/layout-router.tsx @@ -34,7 +34,11 @@ import { RedirectBoundary } from './redirect-boundary' import { getSegmentValue } from './router-reducer/reducers/get-segment-value' import { createRouterCacheKey } from './router-reducer/create-router-cache-key' import { hasInterceptionRouteInCurrentTree } from './router-reducer/reducers/has-interception-route-in-current-tree' -import { ForbiddenBoundary, NotFoundBoundary } from './ui-errors-boundaries' +import { + ForbiddenBoundary, + NotFoundBoundary, + UnauthorizedBoundary, +} from './ui-errors-boundaries' /** * Add refetch marker to router state at the point of the current layout segment. @@ -527,6 +531,8 @@ export default function OuterLayoutRouter({ notFoundStyles, forbidden, forbiddenStyles, + unauthorized, + unauthorizedStyles, styles, }: { parallelRouterKey: string @@ -541,6 +547,8 @@ export default function OuterLayoutRouter({ notFoundStyles: React.ReactNode | undefined forbidden: React.ReactNode | undefined forbiddenStyles: React.ReactNode | undefined + unauthorized: React.ReactNode | undefined + unauthorizedStyles: React.ReactNode | undefined styles?: React.ReactNode }) { const context = useContext(LayoutRouterContext) @@ -604,29 +612,35 @@ export default function OuterLayoutRouter({ loadingStyles={loading?.[1]} loadingScripts={loading?.[2]} > - - - - - - - + + + + + + + diff --git a/packages/next/src/client/components/navigation.react-server.ts b/packages/next/src/client/components/navigation.react-server.ts index 32aa4ffb3b12c..bd38a357ec5a3 100644 --- a/packages/next/src/client/components/navigation.react-server.ts +++ b/packages/next/src/client/components/navigation.react-server.ts @@ -29,4 +29,5 @@ class ReadonlyURLSearchParams extends URLSearchParams { export { redirect, permanentRedirect, RedirectType } from './redirect' export { notFound } from './not-found' export { forbidden } from './forbidden' +export { unauthorized } from './unauthorized' export { ReadonlyURLSearchParams } diff --git a/packages/next/src/client/components/navigation.ts b/packages/next/src/client/components/navigation.ts index a3394ea0229f6..8e0d44d7baa1f 100644 --- a/packages/next/src/client/components/navigation.ts +++ b/packages/next/src/client/components/navigation.ts @@ -266,6 +266,7 @@ export { export { notFound, forbidden, + unauthorized, redirect, permanentRedirect, RedirectType, diff --git a/packages/next/src/client/components/ui-errors-boundaries.tsx b/packages/next/src/client/components/ui-errors-boundaries.tsx index a2d7c2a3936a4..76de8498fc6de 100644 --- a/packages/next/src/client/components/ui-errors-boundaries.tsx +++ b/packages/next/src/client/components/ui-errors-boundaries.tsx @@ -8,6 +8,7 @@ import { } from './ui-error-boundary' import { isForbiddenError } from './forbidden' import { isNotFoundError } from './not-found' +import { isUnauthorizedError } from './unauthorized' type BoundaryConsumerProps = Pick< UIErrorBoundaryWrapperProps, @@ -33,3 +34,18 @@ export function NotFoundBoundary(props: BoundaryConsumerProps) { /> ) } + +type UnauthorizedBoundaryProps = Pick< + UIErrorBoundaryWrapperProps, + 'uiComponent' | 'uiComponentStyles' | 'children' +> + +export function UnauthorizedBoundary(props: UnauthorizedBoundaryProps) { + return ( + + ) +} diff --git a/packages/next/src/client/components/unauthorized-error.tsx b/packages/next/src/client/components/unauthorized-error.tsx new file mode 100644 index 0000000000000..8b49131ee796a --- /dev/null +++ b/packages/next/src/client/components/unauthorized-error.tsx @@ -0,0 +1,11 @@ +import { UIErrorTemplate } from './ui-error-template' + +export default function Unauthorized() { + return ( + + ) +} diff --git a/packages/next/src/client/components/unauthorized.ts b/packages/next/src/client/components/unauthorized.ts new file mode 100644 index 0000000000000..4232dc94232dd --- /dev/null +++ b/packages/next/src/client/components/unauthorized.ts @@ -0,0 +1,11 @@ +import { createUIError } from './ui-error-builder' + +const { thrower, matcher } = createUIError('NEXT_UNAUTHORIZED') + +// TODO(@panteliselef): Update docs +const unauthorized = thrower + +// TODO(@panteliselef): Update docs +const isUnauthorizedError = matcher + +export { unauthorized, isUnauthorizedError } diff --git a/packages/next/src/export/helpers/is-navigation-signal-error.ts b/packages/next/src/export/helpers/is-navigation-signal-error.ts index f67bf8f575735..e705d71f687bf 100644 --- a/packages/next/src/export/helpers/is-navigation-signal-error.ts +++ b/packages/next/src/export/helpers/is-navigation-signal-error.ts @@ -1,6 +1,7 @@ import { isNotFoundError } from '../../client/components/not-found' import { isRedirectError } from '../../client/components/redirect' import { isForbiddenError } from '../../client/components/forbidden' +import { isUnauthorizedError } from '../../client/components/unauthorized' /** * Returns true if the error is a navigation signal error. These errors are @@ -8,4 +9,7 @@ import { isForbiddenError } from '../../client/components/forbidden' * render. */ export const isNavigationSignalError = (err: unknown) => - isNotFoundError(err) || isRedirectError(err) || isForbiddenError(err) + isNotFoundError(err) || + isRedirectError(err) || + isForbiddenError(err) || + isUnauthorizedError(err) diff --git a/packages/next/src/server/app-render/create-component-tree.tsx b/packages/next/src/server/app-render/create-component-tree.tsx index cb0beb441554c..aec159eed653a 100644 --- a/packages/next/src/server/app-render/create-component-tree.tsx +++ b/packages/next/src/server/app-render/create-component-tree.tsx @@ -15,6 +15,7 @@ import { getTracer } from '../lib/trace/tracer' import { NextNodeServerSpan } from '../lib/trace/constants' import { StaticGenBailoutError } from '../../client/components/static-generation-bailout' import type { LoadingModuleData } from '../../shared/lib/app-router-context.shared-runtime' +import { UnauthorizedBoundary } from '../../client/components/ui-errors-boundaries' type ComponentTree = { seedData: CacheNodeSeedData @@ -108,6 +109,7 @@ async function createComponentTreeInternal({ loading, 'not-found': notFound, forbidden, + unauthorized, } = components const injectedCSSWithCurrentLayout = new Set(injectedCSS) @@ -198,6 +200,16 @@ async function createComponentTreeInternal({ }) : [] + const [Unauthorized, unauthorizedStyles] = unauthorized + ? await createComponentStylesAndScripts({ + ctx, + filePath: unauthorized[1], + getComponent: unauthorized[0], + injectedCSS: injectedCSSWithCurrentLayout, + injectedJS: injectedJSWithCurrentLayout, + }) + : [] + let dynamic = layoutOrPageMod?.dynamic if (nextConfigOutput === 'export') { @@ -302,48 +314,64 @@ async function createComponentTreeInternal({ Component = (componentProps: { params: Params }) => { const NotFoundComponent = NotFound const ForbiddenComponent = Forbidden + const UnauthorizedComponent = Unauthorized const RootLayoutComponent = LayoutOrPage - const RoolLayoutBoundaryComponent = () => { - return - } return ( - {layerAssets} {/* * We are intentionally only forwarding params to the root layout, as passing any of the parallel route props - * might trigger `forbidden()`, which is not currently supported in the root layout. + * might trigger `unauthorized()`, which is not currently supported in the root layout. */} - {forbiddenStyles} - + {unauthorizedStyles} + ) : undefined } > - {layerAssets} {/* * We are intentionally only forwarding params to the root layout, as passing any of the parallel route props - * might trigger `notFound()`, which is not currently supported in the root layout. + * might trigger `forbidden()`, which is not currently supported in the root layout. */} - {notFoundStyles} - + {forbiddenStyles} + ) : undefined } > - - - + + {layerAssets} + {/* + * We are intentionally only forwarding params to the root layout, as passing any of the parallel route props + * might trigger `notFound()`, which is not currently supported in the root layout. + */} + + {notFoundStyles} + + + + ) : undefined + } + > + + + + ) } } @@ -385,6 +413,15 @@ async function createComponentTreeInternal({ `The default export of forbidden is not a React Component in ${segment}` ) } + + if ( + typeof Unauthorized !== 'undefined' && + !isValidElementType(Unauthorized) + ) { + throw new Error( + `The default export of unauthorized is not a React Component in ${segment}` + ) + } } // Handle dynamic segment params. @@ -425,6 +462,9 @@ async function createComponentTreeInternal({ const forbiddenComponent = Forbidden && isChildrenRouteKey ? : undefined + const unauthorizedComponent = + Unauthorized && isChildrenRouteKey ? : undefined + // if we're prefetching and that there's a Loading component, we bail out // otherwise we keep rendering for the prefetch. // We also want to bail out if there's no Loading component in the tree. @@ -525,6 +565,8 @@ async function createComponentTreeInternal({ notFoundStyles={notFoundStyles} forbidden={forbiddenComponent} forbiddenStyles={forbiddenStyles} + unauthorized={unauthorizedComponent} + unauthorizedStyles={unauthorizedStyles} styles={currentStyles} />, childCacheNodeSeedData, diff --git a/packages/next/src/server/app-render/entry-base.ts b/packages/next/src/server/app-render/entry-base.ts index 2555ee0c8f045..b4f6430fd93ed 100644 --- a/packages/next/src/server/app-render/entry-base.ts +++ b/packages/next/src/server/app-render/entry-base.ts @@ -30,6 +30,7 @@ import { import { Postpone } from '../../server/app-render/rsc/postpone' import { taintObjectReference } from '../../server/app-render/rsc/taint' import { + UnauthorizedBoundary, ForbiddenBoundary, NotFoundBoundary, } from '../../client/components/ui-errors-boundaries' @@ -56,6 +57,7 @@ export { Postpone, taintObjectReference, ClientPageRoot, + UnauthorizedBoundary, NotFoundBoundary, ForbiddenBoundary, patchFetch, diff --git a/packages/next/src/server/future/route-modules/helpers/respone-ui-errors.ts b/packages/next/src/server/future/route-modules/helpers/respone-ui-errors.ts new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/packages/next/src/shared/lib/ui-error-types.ts b/packages/next/src/shared/lib/ui-error-types.ts index ff26f7b92fa04..921cd08a63408 100644 --- a/packages/next/src/shared/lib/ui-error-types.ts +++ b/packages/next/src/shared/lib/ui-error-types.ts @@ -1,5 +1,6 @@ import { isNotFoundError } from '../../client/components/not-found' import { isForbiddenError } from '../../client/components/forbidden' +import { isUnauthorizedError } from '../../client/components/unauthorized' const uiErrorsWithStatusCodesMap = { 'not-found': { @@ -12,6 +13,11 @@ const uiErrorsWithStatusCodesMap = { matcher: isForbiddenError, helperName: 'forbidden', }, + unauthorized: { + statusCode: 401, + matcher: isUnauthorizedError, + helperName: 'unauthorized', + }, } as const const uiErrorFileTypes = Object.keys(