RouteValidationWrapper.tsx122 lines · main
1import { LOCAL_STORAGE_KEYS, useIsLoggedIn, useIsMFAEnabled, useParams } from 'common'
2import { useRouter } from 'next/router'
3import { PropsWithChildren, useEffect } from 'react'
4import { toast } from 'sonner'
5
6import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
7import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
8import { useDashboardHistory } from '@/hooks/misc/useDashboardHistory'
9import useLatest from '@/hooks/misc/useLatest'
10import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
11import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
12import { IS_PLATFORM } from '@/lib/constants'
13
14// Ideally these could all be within a _middleware when we use Next 12
15export const RouteValidationWrapper = ({ children }: PropsWithChildren<{}>) => {
16 const router = useRouter()
17 const { ref, slug, id } = useParams()
18 const { data: organization } = useSelectedOrganizationQuery()
19
20 const isLoggedIn = useIsLoggedIn()
21 const isUserMFAEnabled = useIsMFAEnabled()
22
23 const { setLastVisitedSnippet, setLastVisitedTable } = useDashboardHistory()
24 const [lastVisitedOrganization, setLastVisitedOrganization] = useLocalStorageQuery(
25 LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
26 ''
27 )
28
29 const DEFAULT_HOME = IS_PLATFORM
30 ? !!lastVisitedOrganization
31 ? `/org/${lastVisitedOrganization}`
32 : '/organizations'
33 : '/project/default'
34
35 /**
36 * Array of urls/routes that should be ignored
37 */
38 const excemptUrls: string[] = [
39 // project creation route, allows the page to self determine it's own route, it will redirect to the first org
40 // or prompt the user to create an organaization
41 // this is used by database.dev, usually as /new/new-project
42 '/new/[slug]',
43 '/join',
44 ]
45
46 /**
47 * Map through all the urls that are excluded
48 * from route validation check
49 *
50 * @returns a boolean
51 */
52 function isExceptUrl() {
53 return excemptUrls.includes(router?.pathname)
54 }
55
56 const { isError: isErrorProject, error: projectError } = useProjectDetailQuery({ ref })
57
58 const { data: organizations, isSuccess: orgsInitialized } = useOrganizationsQuery({
59 enabled: isLoggedIn,
60 })
61 const organizationsRef = useLatest(organizations)
62
63 useEffect(() => {
64 // check if current route is excempted from route validation check
65 if (isExceptUrl() || !isLoggedIn) return
66
67 if (orgsInitialized && slug) {
68 // Check validity of organization that user is trying to access
69 const organizations = organizationsRef.current ?? []
70 const isValidOrg = organizations.some((org) => org.slug === slug)
71
72 if (!isValidOrg) {
73 toast.error('You do not have access to this organization')
74 router.push(`${DEFAULT_HOME}?error=org_not_found&org=${slug}`)
75 return
76 }
77 }
78 }, [orgsInitialized])
79
80 useEffect(() => {
81 // check if current route is excempted from route validation check
82 if (isExceptUrl() || !isLoggedIn) return
83
84 // A successful request to project details will validate access to both project and branches
85 if (!!ref && isErrorProject) {
86 // 404 means the project no longer exists (e.g. was deleted), not an access error
87 if (projectError?.code !== 404) {
88 toast.error('You do not have access to this project')
89 }
90 router.push(DEFAULT_HOME)
91 return
92 }
93 }, [isErrorProject])
94
95 useEffect(() => {
96 if (ref !== undefined && id !== undefined) {
97 if (router.pathname.endsWith('/sql/[id]') && id !== 'new') {
98 setLastVisitedSnippet(id)
99 } else if (router.pathname.endsWith('/editor/[id]')) {
100 setLastVisitedTable(id)
101 }
102 }
103 // eslint-disable-next-line react-hooks/exhaustive-deps
104 }, [ref, id])
105
106 useEffect(() => {
107 if (organization) {
108 setLastVisitedOrganization(organization.slug)
109
110 if (
111 organization.organization_requires_mfa &&
112 !isUserMFAEnabled &&
113 router.pathname !== '/org/[slug]'
114 ) {
115 router.push(`/org/${organization.slug}`)
116 }
117 }
118 // eslint-disable-next-line react-hooks/exhaustive-deps
119 }, [organization])
120
121 return <>{children}</>
122}