Skip to content

Commit

Permalink
feat: Introduce unauthorized()
Browse files Browse the repository at this point in the history
  • Loading branch information
panteliselef committed May 17, 2024
1 parent bc7dfa0 commit 20f2170
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 46 deletions.
2 changes: 2 additions & 0 deletions packages/next/src/build/webpack/loaders/next-app-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type AppLoader = webpack.LoaderDefinitionFunction<AppLoaderOptions>
const UI_FILE_TYPES = {
'not-found': 'not-found',
forbidden: 'forbidden',
unauthorized: 'unauthorized',
} as const

const FILE_TYPES = {
Expand All @@ -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'
Expand Down
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -18,16 +22,23 @@ function NotAllowedRootForbiddenError() {
return null
}

function NotAllowedRootUnauthorizedError() {
bailOnUIError('unauthorized')
return null
}

export function DevRootUIErrorsBoundary({
children,
}: {
children: React.ReactNode
}) {
return (
<ForbiddenBoundary uiComponent={<NotAllowedRootForbiddenError />}>
<NotFoundBoundary uiComponent={<NotAllowedRootNotFoundError />}>
{children}
</NotFoundBoundary>
</ForbiddenBoundary>
<UnauthorizedBoundary uiComponent={<NotAllowedRootUnauthorizedError />}>
<ForbiddenBoundary uiComponent={<NotAllowedRootForbiddenError />}>
<NotFoundBoundary uiComponent={<NotAllowedRootNotFoundError />}>
{children}
</NotFoundBoundary>
</ForbiddenBoundary>
</UnauthorizedBoundary>
)
}
4 changes: 3 additions & 1 deletion packages/next/src/client/components/is-next-router-error.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { isNotFoundError } from './not-found'
import { isRedirectError } from './redirect'
import { isForbiddenError } from './forbidden'
import { isUnauthorizedError } from './unauthorized'

export function isNextRouterError(error: any): boolean {
return (
error &&
error.digest &&
(isRedirectError(error) ||
isNotFoundError(error) ||
isForbiddenError(error))
isForbiddenError(error) ||
isUnauthorizedError(error))
)
}
58 changes: 36 additions & 22 deletions packages/next/src/client/components/layout-router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -527,6 +531,8 @@ export default function OuterLayoutRouter({
notFoundStyles,
forbidden,
forbiddenStyles,
unauthorized,
unauthorizedStyles,
styles,
}: {
parallelRouterKey: string
Expand All @@ -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)
Expand Down Expand Up @@ -604,29 +612,35 @@ export default function OuterLayoutRouter({
loadingStyles={loading?.[1]}
loadingScripts={loading?.[2]}
>
<ForbiddenBoundary
uiComponent={forbidden}
uiComponentStyles={forbiddenStyles}
<UnauthorizedBoundary
uiComponent={unauthorized}
uiComponentStyles={unauthorizedStyles}
>
<NotFoundBoundary
uiComponent={notFound}
uiComponentStyles={notFoundStyles}
<ForbiddenBoundary
uiComponent={forbidden}
uiComponentStyles={forbiddenStyles}
>
<RedirectBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
segmentPath={segmentPath}
cacheKey={cacheKey}
isActive={
currentChildSegmentValue === preservedSegmentValue
}
/>
</RedirectBoundary>
</NotFoundBoundary>
</ForbiddenBoundary>
<NotFoundBoundary
uiComponent={notFound}
uiComponentStyles={notFoundStyles}
>
<RedirectBoundary>
<InnerLayoutRouter
parallelRouterKey={parallelRouterKey}
url={url}
tree={tree}
childNodes={childNodesForParallelRouter!}
segmentPath={segmentPath}
cacheKey={cacheKey}
isActive={
currentChildSegmentValue ===
preservedSegmentValue
}
/>
</RedirectBoundary>
</NotFoundBoundary>
</ForbiddenBoundary>
</UnauthorizedBoundary>
</LoadingBoundary>
</ErrorBoundary>
</ScrollAndFocusHandler>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
1 change: 1 addition & 0 deletions packages/next/src/client/components/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export {
export {
notFound,
forbidden,
unauthorized,
redirect,
permanentRedirect,
RedirectType,
Expand Down
16 changes: 16 additions & 0 deletions packages/next/src/client/components/ui-errors-boundaries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,3 +34,18 @@ export function NotFoundBoundary(props: BoundaryConsumerProps) {
/>
)
}

type UnauthorizedBoundaryProps = Pick<
UIErrorBoundaryWrapperProps,
'uiComponent' | 'uiComponentStyles' | 'children'
>

export function UnauthorizedBoundary(props: UnauthorizedBoundaryProps) {
return (
<UIErrorBoundaryWrapper
nextError={'unauthorized'}
matcher={isUnauthorizedError}
{...props}
/>
)
}
11 changes: 11 additions & 0 deletions packages/next/src/client/components/unauthorized-error.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { UIErrorTemplate } from './ui-error-template'

export default function Unauthorized() {
return (
<UIErrorTemplate
pageTitle="401: Unauthorized Access to this page."
title="401"
subtitle="Unauthorized Access to this page."
/>
)
}
11 changes: 11 additions & 0 deletions packages/next/src/client/components/unauthorized.ts
Original file line number Diff line number Diff line change
@@ -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 }
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
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
* thrown by user code to perform navigation operations and interrupt the React
* render.
*/
export const isNavigationSignalError = (err: unknown) =>
isNotFoundError(err) || isRedirectError(err) || isForbiddenError(err)
isNotFoundError(err) ||
isRedirectError(err) ||
isForbiddenError(err) ||
isUnauthorizedError(err)
74 changes: 58 additions & 16 deletions packages/next/src/server/app-render/create-component-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,6 +109,7 @@ async function createComponentTreeInternal({
loading,
'not-found': notFound,
forbidden,
unauthorized,
} = components

const injectedCSSWithCurrentLayout = new Set(injectedCSS)
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -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 (
<ForbiddenBoundary
<UnauthorizedBoundary
uiComponent={
ForbiddenComponent ? (
UnauthorizedComponent ? (
<>
{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.
*/}
<RootLayoutComponent params={componentProps.params}>
{forbiddenStyles}
<ForbiddenComponent />
{unauthorizedStyles}
<UnauthorizedComponent />
</RootLayoutComponent>
</>
) : undefined
}
>
<NotFoundBoundary
<ForbiddenBoundary
uiComponent={
NotFoundComponent ? (
ForbiddenComponent ? (
<>
{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.
*/}
<RootLayoutComponent params={componentProps.params}>
{notFoundStyles}
<NotFoundComponent />
{forbiddenStyles}
<ForbiddenComponent />
</RootLayoutComponent>
</>
) : undefined
}
>
<RootLayoutComponent {...componentProps} />
</NotFoundBoundary>
</ForbiddenBoundary>
<NotFoundBoundary
uiComponent={
NotFoundComponent ? (
<>
{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.
*/}
<RootLayoutComponent params={componentProps.params}>
{notFoundStyles}
<NotFoundComponent />
</RootLayoutComponent>
</>
) : undefined
}
>
<RootLayoutComponent {...componentProps} />
</NotFoundBoundary>
</ForbiddenBoundary>
</UnauthorizedBoundary>
)
}
}
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -425,6 +462,9 @@ async function createComponentTreeInternal({
const forbiddenComponent =
Forbidden && isChildrenRouteKey ? <Forbidden /> : undefined

const unauthorizedComponent =
Unauthorized && isChildrenRouteKey ? <Unauthorized /> : 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.
Expand Down Expand Up @@ -525,6 +565,8 @@ async function createComponentTreeInternal({
notFoundStyles={notFoundStyles}
forbidden={forbiddenComponent}
forbiddenStyles={forbiddenStyles}
unauthorized={unauthorizedComponent}
unauthorizedStyles={unauthorizedStyles}
styles={currentStyles}
/>,
childCacheNodeSeedData,
Expand Down
Loading

0 comments on commit 20f2170

Please sign in to comment.