mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 18:05:11 +03:00
## What kind of change does this PR introduce? Bug fix. Resolves DEPR-318. ## What is the current behavior? New users who confirm their email land on `/sign-in`, then `/organizations`, then get bounced to `/new` via a `useEffect`. Cancelling org creation with zero orgs sends them back to `/organizations`, which immediately redirects into `/new` again. ## What is the new behavior? - Signup email verification redirects to `/new` directly - `/organizations` with zero orgs shows the existing empty state instead of force-redirecting ## To test ### One-time setup Assuming you don’t already have a staging account with **zero** orgs: 1. On **supabase.green**, sign up with a fresh email and confirm it 2. Stop at org creation. Do **not** create an org ### On this PR Using the [studio-staging preview](https://studio-staging-git-dnywh-fixremove-org-redirect-supabase.vercel.app/) from Vercel checks: 4. Sign in on the preview with that account 5. Open `/dashboard/organizations`. Expect the **Create an organization** empty state, with no redirect to `/new` 6. Open `/dashboard/new`, click **Cancel**. Expect to land on `/organizations` and stay there ### Compare on supabase.green Optional. Just to show what happens currently on `master`: 7. Repeat steps 3–5 on **supabase.green**. `/organizations` should bounce to `/new`, and **Cancel** should send you back into org creation <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Improved signup redirects by preserving valid destinations and relevant query parameters. * Added safer fallback behavior for missing, invalid, or unsupported destinations. * Improved handling of signup redirects provided in multiple formats. * Prevented automatic redirection from the organizations page when no organizations exist. * **Style** * Updated the organizations page title capitalization for consistency. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
154 lines
5.1 KiB
TypeScript
154 lines
5.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildSignUpReturnPath,
|
|
DEFAULT_FALLBACK_PATH,
|
|
DEFAULT_SIGNUP_RETURN_PATH,
|
|
getSignUpReturnTo,
|
|
validateReturnTo,
|
|
} from './gotrue'
|
|
|
|
describe('getSignUpReturnTo', () => {
|
|
it('defaults to /new when returnTo is missing', () => {
|
|
expect(getSignUpReturnTo(undefined)).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
expect(getSignUpReturnTo('')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
|
|
it('rewrites /org and /organizations to /new', () => {
|
|
expect(getSignUpReturnTo('/org')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
expect(getSignUpReturnTo('/organizations')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
|
|
it('rewrites org list paths while preserving query params', () => {
|
|
expect(getSignUpReturnTo('/org?foo=bar')).toBe('/new?foo=bar')
|
|
expect(getSignUpReturnTo('/organizations?foo=bar')).toBe('/new?foo=bar')
|
|
})
|
|
|
|
it('preserves explicit destinations including invite paths', () => {
|
|
expect(getSignUpReturnTo('/join?token=abc')).toBe('/join?token=abc')
|
|
expect(getSignUpReturnTo('/project/ref')).toBe('/project/ref')
|
|
})
|
|
|
|
it('uses the first value when returnTo is an array', () => {
|
|
expect(getSignUpReturnTo(['/join?token=abc', '/other'])).toBe('/join?token=abc')
|
|
expect(getSignUpReturnTo(['/org'])).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
expect(getSignUpReturnTo(['/organizations'])).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
})
|
|
|
|
describe('buildSignUpReturnPath', () => {
|
|
it('defaults to /new when returnTo is missing', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath(undefined)).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
|
|
it('rewrites /org and /organizations to /new', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath('/org')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
expect(buildSignUpReturnPath('/organizations')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
|
|
it('rewrites org list paths while preserving query params', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath('/org?foo=bar')).toBe('/new?foo=bar')
|
|
expect(buildSignUpReturnPath('/organizations?foo=bar')).toBe('/new?foo=bar')
|
|
})
|
|
|
|
it('preserves invite returnTo paths', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath('/join?token=abc')).toBe('/join?token=abc')
|
|
})
|
|
|
|
it('falls back to /new for invalid returnTo values', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath('https://evil.com')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
expect(buildSignUpReturnPath('//evil.com')).toBe(DEFAULT_SIGNUP_RETURN_PATH)
|
|
})
|
|
|
|
it('strips returnTo from the current location search params', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '?returnTo=/org&foo=bar' }
|
|
|
|
expect(buildSignUpReturnPath('/new')).toBe('/new?foo=bar')
|
|
})
|
|
|
|
it('uses the first value when returnTo is an array', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '' }
|
|
|
|
expect(buildSignUpReturnPath(['/join?token=abc', '/other'])).toBe('/join?token=abc')
|
|
})
|
|
})
|
|
|
|
describe('validateReturnTo', () => {
|
|
const fallback = DEFAULT_FALLBACK_PATH
|
|
|
|
it('should return the path if it is a valid internal path', () => {
|
|
expect(validateReturnTo('/dashboard')).toBe('/dashboard')
|
|
expect(validateReturnTo('/settings/profile')).toBe('/settings/profile')
|
|
expect(validateReturnTo('/projects?id=123')).toBe('/projects?id=123')
|
|
})
|
|
|
|
it('should return fallback if given an external URL', () => {
|
|
expect(validateReturnTo('https://example.com')).toBe(fallback)
|
|
expect(validateReturnTo('http://malicious-site.com')).toBe(fallback)
|
|
expect(validateReturnTo('//evil.com')).toBe(fallback)
|
|
})
|
|
|
|
it('should return fallback for potentially malicious paths', () => {
|
|
expect(validateReturnTo('/%2e%2e/etc/passwd')).toBe(fallback)
|
|
expect(validateReturnTo('/..')).toBe(fallback)
|
|
expect(validateReturnTo('/@evil/path')).toBe(fallback)
|
|
expect(validateReturnTo('/$malicious')).toBe(fallback)
|
|
})
|
|
|
|
it('should use custom fallback when provided', () => {
|
|
const customFallback = '/custom-fallback'
|
|
expect(validateReturnTo('https://example.com', customFallback)).toBe(customFallback)
|
|
expect(validateReturnTo('/%2e%2e/etc/passwd', customFallback)).toBe(customFallback)
|
|
})
|
|
|
|
it('should handle paths with query parameters correctly', () => {
|
|
expect(validateReturnTo('/dashboard?param1=value1¶m2=value2')).toBe(
|
|
'/dashboard?param1=value1¶m2=value2'
|
|
)
|
|
})
|
|
|
|
it('should handle nextjs dynamic path js', () => {
|
|
expect(
|
|
validateReturnTo(
|
|
'%2F%5B%5Bx%5D%5Djavascript%3Aalert(%22H4CK3D%22)%2F%5By%5D%2F%5B%5Bx%5D%5D%2F%5By%5D%3Fx%26y'
|
|
)
|
|
).toBe(fallback)
|
|
expect(
|
|
validateReturnTo(
|
|
'/%2F%5B%5Bx%5D%5Djavascript%3Aalert(%22H4CK3D%22)%2F%5By%5D%2F%5B%5Bx%5D%5D%2F%5By%5D%3Fx%26y'
|
|
)
|
|
).toBe(fallback)
|
|
})
|
|
})
|