auth.tsx176 lines · main
1'use client'
2
3import type { AuthError, Session } from '@supabase/supabase-js'
4import {
5 createContext,
6 PropsWithChildren,
7 useCallback,
8 useContext,
9 useEffect,
10 useMemo,
11 useState,
12} from 'react'
13
14import { clearLocalStorage } from './constants/local-storage'
15import { gotrueClient, type User } from './gotrue'
16
17export type { User }
18
19const DEFAULT_SESSION: any = {
20 access_token: undefined,
21 expires_at: 0,
22 expires_in: 0,
23 refresh_token: '',
24 token_type: '',
25 user: {
26 aud: '',
27 app_metadata: {},
28 confirmed_at: '',
29 created_at: '',
30 email: '',
31 email_confirmed_at: '',
32 id: '',
33 identities: [],
34 last_signed_in_at: '',
35 phone: '',
36 role: '',
37 updated_at: '',
38 user_metadata: {},
39 },
40} as unknown as Session
41
42/* Auth Context */
43
44type AuthState =
45 | {
46 session: Session | null
47 error: AuthError | null
48 isLoading: false
49 }
50 | {
51 session: null
52 error: AuthError | null
53 isLoading: true
54 }
55
56export type AuthContext = { refreshSession: () => Promise<Session | null> } & AuthState
57
58export const AuthContext = createContext<AuthContext>({
59 session: null,
60 error: null,
61 isLoading: true,
62 refreshSession: () => Promise.resolve(null),
63})
64
65export type AuthProviderProps = {
66 alwaysLoggedIn?: boolean
67}
68
69export const AuthProvider = ({
70 alwaysLoggedIn,
71 children,
72}: PropsWithChildren<AuthProviderProps>) => {
73 const [state, setState] = useState<AuthState>({ session: null, error: null, isLoading: true })
74
75 useEffect(() => {
76 let mounted = true
77 gotrueClient.initialize().then(({ error }) => {
78 if (mounted && error !== null) {
79 setState((prev) => ({ ...prev, error }))
80 }
81 })
82
83 return () => {
84 mounted = false
85 }
86 }, [])
87
88 // Keep the session in sync
89 useEffect(() => {
90 const {
91 data: { subscription },
92 } = gotrueClient.onAuthStateChange((_event, session) => {
93 setState((prev) => ({
94 session,
95 // If there is a session, we clear the error
96 error: session !== null ? null : prev.error,
97 isLoading: false,
98 }))
99 })
100
101 return subscription.unsubscribe
102 }, [])
103
104 // Helper method to refresh the session.
105 // For example after a user updates their profile
106 const refreshSession = useCallback(async () => {
107 const {
108 data: { session },
109 } = await gotrueClient.refreshSession()
110
111 return session
112 }, [])
113
114 const value = useMemo(() => {
115 if (alwaysLoggedIn) {
116 return { session: DEFAULT_SESSION, error: null, isLoading: false, refreshSession } as const
117 } else {
118 return { ...state, refreshSession } as const
119 }
120 }, [state, refreshSession])
121
122 return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
123}
124
125/* Auth Utils */
126
127export const useAuth = () => useContext(AuthContext)
128
129export const useSession = () => useAuth().session
130
131export const useUser = () => useSession()?.user ?? null
132
133export const useIsUserLoading = () => useAuth().isLoading
134
135export const useIsLoggedIn = () => {
136 const user = useUser()
137
138 return user !== null
139}
140
141export const useAuthError = () => useAuth().error
142
143export const useIsMFAEnabled = () => {
144 const user = useUser()
145
146 return user !== null && user.factors && user.factors.length > 0
147}
148
149export const signOut = async () => await gotrueClient.signOut()
150
151export const logOut = async () => {
152 await signOut()
153 clearLocalStorage()
154}
155
156gotrueClient.onAuthStateChange((_event, _session) => {})
157
158/**
159 * Gets a current access token.
160 *
161 * Calls getSession, which will refresh the token if needed.
162 */
163export async function getAccessToken() {
164 // ignore if server-side
165 if (typeof window === 'undefined') return undefined
166
167 const {
168 data: { session },
169 error,
170 } = await gotrueClient.getSession()
171 if (error) {
172 throw error
173 }
174
175 return session?.access_token
176}