Files
supabase/apps/studio/components/interfaces/App/RouteValidationWrapper.tsx
T
kemal.earthandClaude Opus 5 41e6d489f8 feat(studio): mcp elicitation url mode ui (#49954)
## 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>
2026-09-08 16:40:36 +01:00

122 lines
4.1 KiB
TypeScript

import { useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
import { useRouter } from 'next/router'
import { PropsWithChildren, useEffect } from 'react'
import { toast } from 'sonner'
import { MCP_SECRETS_ROUTE } from '@/components/interfaces/MCP/Secrets/McpSecrets.constants'
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
import { useLastVisitedOrganization } from '@/hooks/misc/useLastVisitedOrganization'
import { useLatest } from '@/hooks/misc/useLatest'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from '@/lib/constants'
// Ideally these could all be within a _middleware when we use Next 12
export const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
const router = useRouter()
const { ref, slug, id } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const isLoggedIn = useIsLoggedIn()
const isUserMFAEnabled = useIsMFAEnabled()
const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
const { lastVisitedOrganization, setLastVisitedOrganization } = useLastVisitedOrganization()
const DEFAULT_HOME = IS_PLATFORM
? !!lastVisitedOrganization
? `/org/${lastVisitedOrganization}`
: '/organizations'
: '/project/default'
/**
* Array of urls/routes that should be ignored
*/
const excemptUrls: string[] = [
// project creation route, allows the page to self determine it's own route, it will redirect to the first org
// or prompt the user to create an organaization
// this is used by database.dev, usually as /new/new-project
'/new/[slug]',
'/join',
MCP_SECRETS_ROUTE,
]
/**
* Map through all the urls that are excluded
* from route validation check
*
* @returns a boolean
*/
function isExceptUrl() {
return excemptUrls.includes(router?.pathname)
}
const { isError: isErrorProject, error: projectError } = useProjectDetailQuery({ ref })
const { data: organizations, isSuccess: orgsInitialized } = useOrganizationsQuery({
enabled: isLoggedIn,
})
const organizationsRef = useLatest(organizations)
useEffect(() => {
// check if current route is excempted from route validation check
if (isExceptUrl() || !isLoggedIn) return
if (orgsInitialized && slug) {
// Check validity of organization that user is trying to access
const organizations = organizationsRef.current ?? []
const isValidOrg = organizations.some((org) => org.slug === slug)
if (!isValidOrg) {
toast.error('You do not have access to this organization')
router.push(DEFAULT_HOME)
return
}
}
}, [orgsInitialized])
useEffect(() => {
// check if current route is excempted from route validation check
if (isExceptUrl() || !isLoggedIn) return
// A successful request to project details will validate access to both project and branches
if (!!ref && isErrorProject) {
// 404 means the project no longer exists (e.g. was deleted), not an access error
if (projectError?.code !== 404) {
toast.error('You do not have access to this project')
}
router.push(DEFAULT_HOME)
return
}
}, [isErrorProject])
useEffect(() => {
if (ref !== undefined && id !== undefined) {
if (router.pathname.endsWith('/sql/[id]') && id !== 'new') {
setLastVisitedSnippet(id)
} else if (router.pathname.endsWith('/editor/[id]')) {
setLastVisitedTable(id)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ref, id])
useEffect(() => {
if (organization) {
setLastVisitedOrganization(organization.slug)
if (
organization.organization_requires_mfa &&
!isUserMFAEnabled &&
router.pathname !== '/org/[slug]'
) {
router.push(`/org/${organization.slug}`)
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [organization])
return <>{children}</>
}