RouteValidationWrapper.tsx 4.1 KB

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