this repo has no description
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};