mirror of
https://github.com/supabase/supabase.git
synced 2026-09-22 13:37:53 +08:00
41e6d489f8
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Initial build of the URL mode interstitial. Has complete mock data to be able to view different states (this will be stripped out in the end). A starting point for us to use as an intercept. This is linked with @barryroodt ticket in the MCP project. https://github.com/supabase/mcp/pull/412 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added an authenticated MCP Secrets page for securely storing API secrets requested by AI tools. - Added provider-aware guidance for OpenAI, Anthropic, Resend, and Stripe keys, including validation warnings and dashboard links. - Added clear success, cancellation, expiration, error, and wrong-account states with account switching. - Added loading placeholders, secret visibility controls, overwrite warnings, and accessibility announcements. - **Bug Fixes** - Improved interstitial animations to respect reduced-motion preferences. - Preserved return destinations and related parameters during sign-in flows. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
175 lines
5.8 KiB
TypeScript
175 lines
5.8 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import {
|
|
buildSignUpReturnPath,
|
|
DEFAULT_FALLBACK_PATH,
|
|
DEFAULT_SIGNUP_RETURN_PATH,
|
|
getReturnToPath,
|
|
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('getReturnToPath', () => {
|
|
it('rebuilds a nested returnTo path with the params that rode alongside it', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '?ref=abc&name=OPENAI_API_KEY&returnTo=%2Fmcp%2Fsecrets' }
|
|
|
|
expect(getReturnToPath()).toBe('/mcp/secrets?ref=abc&name=OPENAI_API_KEY')
|
|
})
|
|
|
|
it('accepts a multi-segment returnTo rather than falling back', () => {
|
|
// @ts-ignore
|
|
delete window.location
|
|
// @ts-ignore
|
|
window.location = { search: '?returnTo=%2Fmcp%2Fsecrets' }
|
|
|
|
expect(getReturnToPath()).toBe('/mcp/secrets')
|
|
})
|
|
})
|
|
|
|
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)
|
|
})
|
|
})
|