A social knowledge tool for researchers built on ATProto
at ff03e09bfaf3b3baf2f90cdc6562677f0331ff67 220 lines 6.4 kB view raw
1import { 2 useState, 3 useEffect, 4 createContext, 5 useContext, 6 ReactNode, 7 useCallback, 8} from 'react'; 9import { ApiClient } from '@/api-client/ApiClient'; 10 11interface ExtensionAuthContextType { 12 isAuthenticated: boolean; 13 isLoading: boolean; 14 accessToken: string | null; 15 user: any | null; 16 loginWithAppPassword: (handle: string, password: string) => Promise<void>; 17 logout: () => Promise<void>; 18 error: string | null; 19} 20 21const ExtensionAuthContext = createContext< 22 ExtensionAuthContextType | undefined 23>(undefined); 24 25export const ExtensionAuthProvider = ({ 26 children, 27}: { 28 children: ReactNode; 29}) => { 30 const [isAuthenticated, setIsAuthenticated] = useState(false); 31 const [isLoading, setIsLoading] = useState(true); 32 const [accessToken, setAccessToken] = useState<string | null>(null); 33 const [user, setUser] = useState<any | null>(null); 34 const [error, setError] = useState<string | null>(null); 35 36 const createApiClient = useCallback((token: string | null) => { 37 return new ApiClient( 38 process.env.PLASMO_PUBLIC_API_URL || 'http://127.0.0.1:3000', 39 ); 40 }, []); 41 42 // Use chrome.storage instead of localStorage 43 const getStoredToken = useCallback(async (): Promise<string | null> => { 44 return new Promise((resolve) => { 45 if (typeof chrome !== 'undefined' && chrome.storage) { 46 chrome.storage.local.get(['accessToken'], (result) => { 47 resolve(result.accessToken || null); 48 }); 49 } else { 50 // Fallback to localStorage for development 51 resolve(localStorage.getItem('accessToken')); 52 } 53 }); 54 }, []); 55 56 const getStoredRefreshToken = useCallback(async (): Promise< 57 string | null 58 > => { 59 return new Promise((resolve) => { 60 if (typeof chrome !== 'undefined' && chrome.storage) { 61 chrome.storage.local.get(['refreshToken'], (result) => { 62 resolve(result.refreshToken || null); 63 }); 64 } else { 65 // Fallback to localStorage for development 66 resolve(localStorage.getItem('refreshToken')); 67 } 68 }); 69 }, []); 70 71 const setStoredToken = useCallback(async (token: string | null) => { 72 if (typeof chrome !== 'undefined' && chrome.storage) { 73 if (token) { 74 chrome.storage.local.set({ accessToken: token }); 75 } else { 76 chrome.storage.local.remove(['accessToken']); 77 } 78 } else { 79 // Fallback to localStorage for development 80 if (token) { 81 localStorage.setItem('accessToken', token); 82 } else { 83 localStorage.removeItem('accessToken'); 84 } 85 } 86 }, []); 87 88 // Helper function to initialize auth state (extracted for reuse) 89 const initAuth = useCallback(async () => { 90 try { 91 const token = await getStoredToken(); 92 if (token) { 93 setAccessToken(token); 94 const apiClient = createApiClient(token); 95 const userData = await apiClient.getMyProfile(); 96 setUser(userData); 97 setIsAuthenticated(true); 98 } else { 99 // No token found, ensure we're in unauthenticated state 100 setAccessToken(null); 101 setUser(null); 102 setIsAuthenticated(false); 103 } 104 setError(null); 105 } catch (error) { 106 console.error('Auth initialization failed:', error); 107 // Token invalid, clear it 108 await setStoredToken(null); 109 setAccessToken(null); 110 setUser(null); 111 setIsAuthenticated(false); 112 setError('Session expired. Please sign in again.'); 113 } finally { 114 setIsLoading(false); 115 } 116 }, [getStoredToken, setStoredToken, createApiClient]); 117 118 // Initialize auth state on mount 119 useEffect(() => { 120 initAuth(); 121 }, [initAuth]); 122 123 // Listen for auth state changes from background script 124 useEffect(() => { 125 const handleAuthStateChange = (message: any) => { 126 if (message.type === 'AUTH_STATE_CHANGED') { 127 // Reload auth state from storage when background script notifies us 128 initAuth(); 129 } 130 }; 131 132 if (typeof chrome !== 'undefined' && chrome.runtime?.onMessage) { 133 chrome.runtime.onMessage.addListener(handleAuthStateChange); 134 135 return () => { 136 chrome.runtime.onMessage.removeListener(handleAuthStateChange); 137 }; 138 } 139 }, [initAuth]); 140 141 const loginWithAppPassword = useCallback( 142 async (identifier: string, appPassword: string) => { 143 try { 144 setError(null); 145 setIsLoading(true); 146 147 // Use unauthenticated client for login 148 const unauthenticatedClient = createApiClient(null); 149 const response = await unauthenticatedClient.loginWithAppPassword({ 150 identifier, 151 appPassword, 152 }); 153 const { accessToken: newToken } = response; 154 155 setAccessToken(newToken); 156 await setStoredToken(newToken); 157 158 // Create new authenticated client for profile fetch 159 const authenticatedClient = createApiClient(newToken); 160 const userData = await authenticatedClient.getMyProfile(); 161 setUser(userData); 162 setIsAuthenticated(true); 163 } catch (error: any) { 164 console.error('App password login failed:', error); 165 setError( 166 error.message || 'Login failed. Please check your credentials.', 167 ); 168 throw error; 169 } finally { 170 setIsLoading(false); 171 } 172 }, 173 [createApiClient, setStoredToken], 174 ); 175 176 const logout = useCallback(async () => { 177 try { 178 // Notify background script to handle logout 179 if (typeof chrome !== 'undefined' && chrome.runtime) { 180 chrome.runtime.sendMessage({ type: 'LOGOUT' }); 181 } else { 182 // Fallback for development 183 setAccessToken(null); 184 setUser(null); 185 setIsAuthenticated(false); 186 setError(null); 187 await setStoredToken(null); 188 localStorage.removeItem('refreshToken'); 189 } 190 } catch (error) { 191 console.error('Logout failed:', error); 192 } 193 }, [setStoredToken]); 194 195 return ( 196 <ExtensionAuthContext.Provider 197 value={{ 198 isAuthenticated, 199 isLoading, 200 accessToken, 201 user, 202 loginWithAppPassword, 203 logout, 204 error, 205 }} 206 > 207 {children} 208 </ExtensionAuthContext.Provider> 209 ); 210}; 211 212export const useExtensionAuth = () => { 213 const context = useContext(ExtensionAuthContext); 214 if (!context) { 215 throw new Error( 216 'useExtensionAuth must be used within ExtensionAuthProvider', 217 ); 218 } 219 return context; 220};