this repo has no description
at main 235 lines 5.5 kB view raw
1/** 2 * Authentication Context for ATProtocol OAuth 3 * Manages auth state, login/logout, and token refresh 4 */ 5 6import React, { 7 createContext, 8 useContext, 9 useState, 10 useEffect, 11 useCallback, 12 ReactNode, 13} from 'react'; 14 15import { ATProtoOAuthClient } from '../lib/atproto-oauth-client'; 16import { XRPCClient } from '../lib/xrpc-client'; 17 18import type { 19 OAuthSession, 20 UserProfile, 21 AuthContextType, 22} from '../types/auth'; 23 24const AuthContext = createContext<AuthContextType | null>(null); 25 26interface AuthProviderProps { 27 children: ReactNode; 28} 29 30export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => { 31 const [user, setUser] = useState<UserProfile | null>(null); 32 const [session, setSession] = useState<OAuthSession | null>(null); 33 const [loading, setLoading] = useState(true); 34 const [error, setError] = useState<Error | null>(null); 35 36 // Create stable client instances (only once) 37 const [oauthClient] = useState(() => new ATProtoOAuthClient()); 38 const [xrpcClient] = useState(() => new XRPCClient()); 39 40 /** 41 * Initialize auth state on mount 42 */ 43 useEffect(() => { 44 const initAuth = async () => { 45 try { 46 const storedSession = await oauthClient.getSession(); 47 48 if (storedSession) { 49 // Session exists, restore user from session data 50 setSession(storedSession); 51 setUser({ 52 did: storedSession.did, 53 handle: storedSession.handle, 54 }); 55 } 56 } catch (err) { 57 console.error('Auth initialization error:', err); 58 setError(err as Error); 59 } finally { 60 setLoading(false); 61 } 62 }; 63 64 initAuth(); 65 }, [oauthClient, xrpcClient]); 66 67 /** 68 * Start OAuth login flow 69 * @param handle - User's handle 70 */ 71 const login = useCallback(async (handle: string) => { 72 try { 73 setLoading(true); 74 setError(null); 75 76 const authUrl = await oauthClient.authorize(handle); 77 78 // Redirect to authorization server 79 window.location.href = authUrl; 80 } catch (err) { 81 console.error('Login error:', err); 82 setError(err as Error); 83 setLoading(false); 84 throw err; 85 } 86 }, [oauthClient]); 87 88 /** 89 * Handle OAuth callback 90 * @param code - Authorization code 91 * @param state - State parameter 92 * @param iss - Optional issuer parameter 93 */ 94 const handleCallback = useCallback(async ( 95 code: string, 96 state: string, 97 iss?: string 98 ) => { 99 try { 100 setLoading(true); 101 setError(null); 102 103 const newSession = await oauthClient.callback(code, state, iss); 104 105 // Set session and user from the session data 106 setSession(newSession); 107 setUser({ 108 did: newSession.did, 109 handle: newSession.handle, 110 }); 111 } catch (err) { 112 console.error('Callback error:', err); 113 setError(err as Error); 114 throw err; 115 } finally { 116 setLoading(false); 117 } 118 }, [oauthClient, xrpcClient]); 119 120 /** 121 * Logout and clear session 122 */ 123 const logout = useCallback(async () => { 124 try { 125 setLoading(true); 126 127 await oauthClient.clearSession(); 128 129 setUser(null); 130 setSession(null); 131 setError(null); 132 } catch (err) { 133 console.error('Logout error:', err); 134 setError(err as Error); 135 } finally { 136 setLoading(false); 137 } 138 }, [oauthClient]); 139 140 /** 141 * Manually refresh access token 142 */ 143 const refreshToken = useCallback(async () => { 144 if (!session) { 145 throw new Error('No active session'); 146 } 147 148 try { 149 const newSession = await oauthClient.refreshAccessToken(session); 150 setSession(newSession); 151 } catch (err) { 152 console.error('Token refresh error:', err); 153 setError(err as Error); 154 throw err; 155 } 156 }, [session, oauthClient]); 157 158 /** 159 * Auto-refresh token before expiry 160 */ 161 useEffect(() => { 162 if (!session) { 163 return; 164 } 165 166 // Calculate time until token expiry 167 const timeUntilExpiry = session.expiresAt - Date.now(); 168 169 // If token is already expired or expiring in less than 1 minute 170 if (timeUntilExpiry <= 60000) { 171 refreshToken().catch((err) => { 172 console.error('Auto-refresh failed:', err); 173 // If refresh fails, clear session 174 logout(); 175 }); 176 return; 177 } 178 179 // Set timeout to refresh 1 minute before expiry 180 const refreshTimeout = setTimeout(() => { 181 refreshToken().catch((err) => { 182 console.error('Auto-refresh failed:', err); 183 logout(); 184 }); 185 }, timeUntilExpiry - 60000); 186 187 return () => clearTimeout(refreshTimeout); 188 }, [session, refreshToken, logout]); 189 190 const contextValue: AuthContextType = { 191 user, 192 session, 193 loading, 194 error, 195 isAuthenticated: !!user && !!session, 196 login, 197 handleCallback, 198 logout, 199 refreshToken, 200 }; 201 202 return ( 203 <AuthContext.Provider value={contextValue}> 204 {children} 205 </AuthContext.Provider> 206 ); 207}; 208 209/** 210 * Hook to use auth context 211 * @returns Auth context value 212 */ 213export const useAuth = (): AuthContextType => { 214 const context = useContext(AuthContext); 215 if (!context) { 216 throw new Error('useAuth must be used within AuthProvider'); 217 } 218 return context; 219}; 220 221/** 222 * Get XRPC client with current session 223 * @returns Configured XRPC client 224 */ 225export const useXRPC = (): XRPCClient | null => { 226 const { session } = useAuth(); 227 228 if (!session) { 229 return null; 230 } 231 232 const client = new XRPCClient(); 233 client.setSession(session); 234 return client; 235};