withAuth.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import { useAuth } from 'common'
  2. import { useRouter } from 'next/router'
  3. import { ComponentType, useCallback, useEffect, useRef, useState } from 'react'
  4. import { toast } from 'sonner'
  5. import { SessionTimeoutModal } from '@/components/interfaces/SignIn/SessionTimeoutModal'
  6. import { usePermissionsQuery } from '@/data/permissions/permissions-query'
  7. import { useAuthenticatorAssuranceLevelQuery } from '@/data/profile/mfa-authenticator-assurance-level-query'
  8. import { useSignOut } from '@/lib/auth'
  9. import { BASE_PATH, IS_PLATFORM } from '@/lib/constants'
  10. import { isNextPageWithLayout, type NextPageWithLayout } from '@/types'
  11. const MAX_TIMEOUT = 10000 // 10 seconds
  12. export function withAuth<T>(
  13. WrappedComponent: ComponentType<T> | NextPageWithLayout<T, T>,
  14. options: {
  15. /**
  16. * The auth level used to check the user credentials. In most cases, if the user has MFA enabled
  17. * we want the highest level (which is 2) for all pages. For certain pages, the user should be
  18. * able to access them even if he didn't finished his login (typed in his MFA code), for example
  19. * the support page: We want the user to be able to submit a ticket even if he's not fully
  20. * signed in.
  21. * @default true
  22. */
  23. useHighestAAL: boolean
  24. } = { useHighestAAL: true }
  25. ) {
  26. // ignore auth in self-hosted
  27. if (!IS_PLATFORM) {
  28. return WrappedComponent
  29. }
  30. const WithAuthHOC: ComponentType<T> = (props) => {
  31. const router = useRouter()
  32. const signOut = useSignOut()
  33. const { isLoading, session } = useAuth()
  34. const timeoutIdRef = useRef<NodeJS.Timeout | null>(null)
  35. const [isSessionTimeoutModalOpen, setIsSessionTimeoutModalOpen] = useState(false)
  36. const {
  37. isPending: isAALLoading,
  38. data: aalData,
  39. isError: isErrorAAL,
  40. error: errorAAL,
  41. } = useAuthenticatorAssuranceLevelQuery()
  42. useEffect(() => {
  43. if (isErrorAAL) {
  44. toast.error(
  45. `Failed to fetch authenticator assurance level: ${errorAAL?.message}. Try refreshing your browser, or reach out to us via a support ticket if the issue persists`
  46. )
  47. }
  48. }, [isErrorAAL, errorAAL])
  49. const { isError: isErrorPermissions, error: errorPermissions } = usePermissionsQuery()
  50. useEffect(() => {
  51. if (isErrorPermissions) {
  52. toast.error(
  53. `Failed to fetch permissions: ${errorPermissions?.message}. Try refreshing your browser, or reach out to us via a support ticket if the issue persists`
  54. )
  55. }
  56. }, [isErrorPermissions, errorPermissions])
  57. const isLoggedIn = Boolean(session)
  58. const isFinishedLoading = !isLoading && !isAALLoading
  59. const redirectToSignIn = useCallback(() => {
  60. let pathname = location.pathname
  61. if (BASE_PATH) {
  62. pathname = pathname.replace(BASE_PATH, '')
  63. }
  64. if (pathname === '/sign-in') {
  65. // If the user is already on the sign in page, we don't need to redirect them
  66. return
  67. }
  68. const searchParams = new URLSearchParams(location.search)
  69. searchParams.set('returnTo', pathname)
  70. // Sign out before redirecting to sign in page incase the user is stuck in a loading state
  71. signOut().finally(() => {
  72. router.push(`/sign-in?${searchParams.toString()}`)
  73. })
  74. }, [router, signOut])
  75. useEffect(() => {
  76. if (!isFinishedLoading) {
  77. timeoutIdRef.current = setTimeout(() => {
  78. setIsSessionTimeoutModalOpen(true)
  79. }, MAX_TIMEOUT)
  80. } else {
  81. if (timeoutIdRef.current) {
  82. clearTimeout(timeoutIdRef.current)
  83. timeoutIdRef.current = null
  84. }
  85. }
  86. return () => {
  87. if (timeoutIdRef.current) {
  88. clearTimeout(timeoutIdRef.current)
  89. }
  90. }
  91. }, [isFinishedLoading, router, redirectToSignIn])
  92. const isCorrectLevel = options.useHighestAAL
  93. ? aalData?.currentLevel === aalData?.nextLevel
  94. : true
  95. const shouldRedirect = isFinishedLoading && (!isLoggedIn || !isCorrectLevel)
  96. useEffect(() => {
  97. if (shouldRedirect) {
  98. // Clear the timeout if it's still active and we are redirecting
  99. if (timeoutIdRef.current) {
  100. clearTimeout(timeoutIdRef.current)
  101. timeoutIdRef.current = null
  102. }
  103. redirectToSignIn()
  104. }
  105. }, [redirectToSignIn, shouldRedirect])
  106. const InnerComponent = WrappedComponent as any
  107. const supportContext =
  108. typeof router.query.ref === 'string' && router.pathname.startsWith('/project/')
  109. ? {
  110. projectRef: router.query.ref,
  111. ...(typeof router.query.organizationSlug === 'string' && {
  112. orgSlug: router.query.organizationSlug,
  113. }),
  114. }
  115. : undefined
  116. return (
  117. <>
  118. <SessionTimeoutModal
  119. visible={isSessionTimeoutModalOpen}
  120. onClose={() => setIsSessionTimeoutModalOpen(false)}
  121. redirectToSignIn={redirectToSignIn}
  122. supportContext={supportContext}
  123. />
  124. <InnerComponent {...props} />
  125. </>
  126. )
  127. }
  128. WithAuthHOC.displayName = `withAuth(${WrappedComponent.displayName})`
  129. if (isNextPageWithLayout(WrappedComponent)) {
  130. ;(WithAuthHOC as NextPageWithLayout<T, T>).getLayout = WrappedComponent.getLayout
  131. }
  132. return WithAuthHOC
  133. }