Skip to content

Commit 7e873a9

Browse files
feat: moves getSafeRedirect into payload package (#12593)
1 parent d85909e commit 7e873a9

File tree

5 files changed

+40
-26
lines changed

5 files changed

+40
-26
lines changed

packages/next/src/views/Login/LoginForm/index.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@ import {
1616
useConfig,
1717
useTranslation,
1818
} from '@payloadcms/ui'
19-
import { formatAdminURL, getLoginOptions } from 'payload/shared'
19+
import { formatAdminURL, getLoginOptions, getSafeRedirect } from 'payload/shared'
2020

2121
import type { LoginFieldProps } from '../LoginField/index.js'
2222

23-
import { getSafeRedirect } from '../../../utilities/getSafeRedirect.js'
2423
import { LoginField } from '../LoginField/index.js'
2524
import './index.scss'
2625

@@ -92,7 +91,7 @@ export const LoginForm: React.FC<{
9291
initialState={initialState}
9392
method="POST"
9493
onSuccess={handleLogin}
95-
redirect={getSafeRedirect(searchParams?.redirect, adminRoute)}
94+
redirect={getSafeRedirect({ fallbackTo: adminRoute, redirectTo: searchParams?.redirect })}
9695
waitForAutocomplete
9796
>
9897
<div className={`${baseClass}__inputWrap`}>

packages/next/src/views/Login/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import type { AdminViewServerProps, ServerProps } from 'payload'
22

33
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
44
import { redirect } from 'next/navigation.js'
5+
import { getSafeRedirect } from 'payload/shared'
56
import React, { Fragment } from 'react'
67

78
import { Logo } from '../../elements/Logo/index.js'
8-
import { getSafeRedirect } from '../../utilities/getSafeRedirect.js'
99
import { LoginForm } from './LoginForm/index.js'
1010
import './index.scss'
1111
export const loginBaseClass = 'login'
@@ -25,7 +25,7 @@ export function LoginView({ initPageResult, params, searchParams }: AdminViewSer
2525
routes: { admin },
2626
} = config
2727

28-
const redirectUrl = getSafeRedirect(searchParams.redirect, admin)
28+
const redirectUrl = getSafeRedirect({ fallbackTo: admin, redirectTo: searchParams.redirect })
2929

3030
if (user) {
3131
redirect(redirectUrl)

packages/payload/src/exports/shared.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export {
3838
} from '../fields/config/types.js'
3939

4040
export { getFieldPaths } from '../fields/getFieldPaths.js'
41-
4241
export * from '../fields/validations.js'
4342

4443
export type {
@@ -50,21 +49,21 @@ export type {
5049
GetFolderDataResult,
5150
Subfolder,
5251
} from '../folders/types.js'
53-
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
5452

53+
export { formatFolderOrDocumentItem } from '../folders/utils/formatFolderOrDocumentItem.js'
5554
export { validOperators, validOperatorSet } from '../types/constants.js'
5655

5756
export { formatFilesize } from '../uploads/formatFilesize.js'
58-
export { isImage } from '../uploads/isImage.js'
5957

58+
export { isImage } from '../uploads/isImage.js'
6059
export { combineWhereConstraints } from '../utilities/combineWhereConstraints.js'
60+
6161
export {
6262
deepCopyObject,
6363
deepCopyObjectComplex,
6464
deepCopyObjectSimple,
6565
deepCopyObjectSimpleWithoutReactComponents,
6666
} from '../utilities/deepCopyObject.js'
67-
6867
export {
6968
deepMerge,
7069
deepMergeWithCombinedArrays,
@@ -75,16 +74,18 @@ export {
7574
export { extractID } from '../utilities/extractID.js'
7675

7776
export { fieldSchemaToJSON } from '../utilities/fieldSchemaToJSON.js'
77+
7878
export { flattenAllFields } from '../utilities/flattenAllFields.js'
7979
export { default as flattenTopLevelFields } from '../utilities/flattenTopLevelFields.js'
80-
8180
export { formatAdminURL } from '../utilities/formatAdminURL.js'
81+
8282
export { formatLabels, toWords } from '../utilities/formatLabels.js'
8383
export { getBestFitFromSizes } from '../utilities/getBestFitFromSizes.js'
8484
export { getDataByPath } from '../utilities/getDataByPath.js'
85-
8685
export { getFieldPermissions } from '../utilities/getFieldPermissions.js'
8786

87+
export { getSafeRedirect } from '../utilities/getSafeRedirect.js'
88+
8889
export { getSelectMode } from '../utilities/getSelectMode.js'
8990

9091
export { getSiblingData } from '../utilities/getSiblingData.js'

packages/next/src/utilities/getSafeRedirect.spec.ts renamed to packages/payload/src/utilities/getSafeRedirect.spec.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('getSafeRedirect', () => {
88
'should allow safe relative path: %s',
99
(input) => {
1010
// If the input is a clean relative path, it should be returned as-is
11-
expect(getSafeRedirect(input, fallback)).toBe(input)
11+
expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(input)
1212
},
1313
)
1414

@@ -17,7 +17,7 @@ describe('getSafeRedirect', () => {
1717
'should fallback on invalid or non-string input: %s',
1818
(input) => {
1919
// If the input is not a valid string, it should return the fallback
20-
expect(getSafeRedirect(input as any, fallback)).toBe(fallback)
20+
expect(getSafeRedirect({ redirectTo: input as any, fallbackTo: fallback })).toBe(fallback)
2121
},
2222
)
2323

@@ -36,20 +36,24 @@ describe('getSafeRedirect', () => {
3636
'%2Fjavascript:alert(1)', // encoded JavaScript scheme
3737
])('should block unsafe redirect: %s', (input) => {
3838
// All of these should return the fallback because they’re unsafe
39-
expect(getSafeRedirect(input, fallback)).toBe(fallback)
39+
expect(getSafeRedirect({ redirectTo: input, fallbackTo: fallback })).toBe(fallback)
4040
})
4141

4242
// Input with extra spaces should still be properly handled
4343
it('should trim whitespace before evaluating', () => {
4444
// A valid path with surrounding spaces should still be accepted
45-
expect(getSafeRedirect(' /dashboard ', fallback)).toBe('/dashboard')
45+
expect(getSafeRedirect({ redirectTo: ' /dashboard ', fallbackTo: fallback })).toBe(
46+
'/dashboard',
47+
)
4648

4749
// An unsafe path with spaces should still be rejected
48-
expect(getSafeRedirect(' //example.com ', fallback)).toBe(fallback)
50+
expect(getSafeRedirect({ redirectTo: ' //example.com ', fallbackTo: fallback })).toBe(
51+
fallback,
52+
)
4953
})
5054

5155
// If decoding the input fails (e.g., invalid percent encoding), it should not crash
5256
it('should return fallback on invalid encoding', () => {
53-
expect(getSafeRedirect('%E0%A4%A', fallback)).toBe(fallback)
57+
expect(getSafeRedirect({ redirectTo: '%E0%A4%A', fallbackTo: fallback })).toBe(fallback)
5458
})
5559
})

packages/next/src/utilities/getSafeRedirect.ts renamed to packages/payload/src/utilities/getSafeRedirect.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
1-
export const getSafeRedirect = (
2-
redirectParam: string | string[],
3-
fallback: string = '/',
4-
): string => {
5-
if (typeof redirectParam !== 'string') {
6-
return fallback
1+
export const getSafeRedirect = ({
2+
allowAbsoluteUrls = false,
3+
fallbackTo = '/',
4+
redirectTo,
5+
}: {
6+
allowAbsoluteUrls?: boolean
7+
fallbackTo?: string
8+
redirectTo: string | string[]
9+
}): string => {
10+
if (typeof redirectTo !== 'string') {
11+
return fallbackTo
712
}
813

914
// Normalize and decode the path
1015
let redirectPath: string
1116
try {
12-
redirectPath = decodeURIComponent(redirectParam.trim())
17+
redirectPath = decodeURIComponent(redirectTo.trim())
1318
} catch {
14-
return fallback // invalid encoding
19+
return fallbackTo // invalid encoding
1520
}
1621

1722
const isSafeRedirect =
@@ -30,5 +35,10 @@ export const getSafeRedirect = (
3035
// Prevent attempts to redirect to full URLs using "/http:" or "/https:"
3136
!redirectPath.toLowerCase().startsWith('/http')
3237

33-
return isSafeRedirect ? redirectPath : fallback
38+
const isAbsoluteSafeRedirect =
39+
allowAbsoluteUrls &&
40+
// Must be a valid absolute URL with http or https
41+
/^https?:\/\/\S+$/i.test(redirectPath)
42+
43+
return isSafeRedirect || isAbsoluteSafeRedirect ? redirectPath : fallbackTo
3444
}

0 commit comments

Comments
 (0)