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