This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

fix session issue

+259 -9
+7 -4
app/src/app/page.tsx
··· 22 22 23 23 export default function Home() { 24 24 const router = useRouter(); 25 - const { isAuthenticated, accessToken, did, handle, serializedKeyPair, dpopNonce, pdsEndpoint, clearAuth } = useAuth(); 25 + const { isAuthenticated, accessToken, refreshToken, did, handle, serializedKeyPair, dpopNonce, pdsEndpoint, clearAuth } = useAuth(); 26 26 27 27 // Status update state 28 28 const [text, setText] = useState('is '); ··· 130 130 ); 131 131 const keyPair = { publicKey, privateKey }; 132 132 133 - // First, check if auth is valid 133 + // First, check if auth is valid (passing the refresh token) 134 134 const isAuthValid = await checkAuth( 135 135 accessToken, 136 136 keyPair, 137 137 did, 138 138 dpopNonce || null, 139 - pdsEndpoint 139 + pdsEndpoint, 140 + refreshToken // Pass the refresh token to enable auto-refresh if needed 140 141 ); 141 142 142 143 if (!isAuthValid) { ··· 167 168 formattedText, 168 169 selectedEmoji, 169 170 dpopNonce || null, 170 - pdsEndpoint 171 + pdsEndpoint, 172 + 0, // initial retry count 173 + refreshToken // Pass refresh token to enable token refresh if needed 171 174 ); 172 175 173 176 console.log('Status update result:', result);
+252 -5
app/src/lib/bluesky-api.ts
··· 14 14 createdAt: string; 15 15 } 16 16 17 + // Check if a JWT token is expired 18 + export function isTokenExpired(token: string): boolean { 19 + try { 20 + // Extract the payload from the JWT token 21 + const parts = token.split('.'); 22 + if (parts.length !== 3) { 23 + console.error('Invalid token format'); 24 + return true; // Assume expired if format is invalid 25 + } 26 + 27 + // Decode the payload 28 + const payload = JSON.parse(atob(parts[1])); 29 + 30 + // Check if the token has an expiration time 31 + if (!payload.exp) { 32 + console.warn('Token does not have an expiration time'); 33 + return false; // Can't determine if it's expired 34 + } 35 + 36 + // Check if the token is expired 37 + const now = Math.floor(Date.now() / 1000); 38 + const isExpired = payload.exp <= now; 39 + 40 + if (isExpired) { 41 + console.log(`Token expired at ${new Date(payload.exp * 1000).toISOString()}`); 42 + } 43 + 44 + return isExpired; 45 + } catch (error) { 46 + console.error('Error checking token expiration:', error); 47 + return true; // Assume expired if there's an error 48 + } 49 + } 50 + 51 + // Refresh an access token using the refresh token 52 + export async function refreshAccessToken( 53 + refreshToken: string, 54 + keyPair: CryptoKeyPair, 55 + pdsEndpoint: string 56 + ): Promise<{ 57 + accessToken: string; 58 + refreshToken: string; 59 + dpopNonce?: string; 60 + }> { 61 + try { 62 + if (!pdsEndpoint) { 63 + throw new Error('No PDS endpoint provided for token refresh'); 64 + } 65 + 66 + console.log('Refreshing access token with PDS endpoint:', pdsEndpoint); 67 + 68 + // Endpoint for token refresh 69 + const tokenEndpoint = `${pdsEndpoint}/oauth/token`; 70 + 71 + // Get a nonce for the DPoP token 72 + let dpopNonce = null; 73 + try { 74 + // Try to get a nonce with a HEAD request 75 + const headResponse = await fetch(tokenEndpoint, { 76 + method: 'HEAD' 77 + }); 78 + 79 + dpopNonce = headResponse.headers.get('DPoP-Nonce'); 80 + if (dpopNonce) { 81 + console.log('Got nonce for token refresh:', dpopNonce); 82 + } 83 + } catch (nonceError) { 84 + console.warn('Failed to get nonce for token refresh:', nonceError); 85 + } 86 + 87 + // Generate DPoP token for the refresh request 88 + const publicKey = await exportJWK(keyPair.publicKey); 89 + const dpopToken = await generateDPoPToken( 90 + keyPair.privateKey, 91 + publicKey, 92 + 'POST', 93 + tokenEndpoint, 94 + dpopNonce 95 + ); 96 + 97 + // Make the token refresh request 98 + const response = await fetch(tokenEndpoint, { 99 + method: 'POST', 100 + headers: { 101 + 'Content-Type': 'application/x-www-form-urlencoded', 102 + 'DPoP': dpopToken 103 + }, 104 + body: new URLSearchParams({ 105 + 'grant_type': 'refresh_token', 106 + 'refresh_token': refreshToken, 107 + 'client_id': 'https://flushing.im/client-metadata.json' 108 + }) 109 + }); 110 + 111 + // Check for nonce error 112 + if (response.status === 401) { 113 + const newNonce = response.headers.get('DPoP-Nonce'); 114 + if (newNonce) { 115 + console.log('Got new nonce during token refresh:', newNonce); 116 + 117 + // Try again with the new nonce 118 + const retryDpopToken = await generateDPoPToken( 119 + keyPair.privateKey, 120 + publicKey, 121 + 'POST', 122 + tokenEndpoint, 123 + newNonce 124 + ); 125 + 126 + const retryResponse = await fetch(tokenEndpoint, { 127 + method: 'POST', 128 + headers: { 129 + 'Content-Type': 'application/x-www-form-urlencoded', 130 + 'DPoP': retryDpopToken 131 + }, 132 + body: new URLSearchParams({ 133 + 'grant_type': 'refresh_token', 134 + 'refresh_token': refreshToken, 135 + 'client_id': 'https://flushing.im/client-metadata.json' 136 + }) 137 + }); 138 + 139 + if (!retryResponse.ok) { 140 + const errorText = await retryResponse.text(); 141 + throw new Error(`Token refresh retry failed: ${retryResponse.status}, ${errorText}`); 142 + } 143 + 144 + const tokenData = await retryResponse.json(); 145 + 146 + // Return the new tokens and nonce 147 + return { 148 + accessToken: tokenData.access_token, 149 + refreshToken: tokenData.refresh_token || refreshToken, // Use the new refresh token if provided 150 + dpopNonce: newNonce 151 + }; 152 + } 153 + } 154 + 155 + if (!response.ok) { 156 + const errorText = await response.text(); 157 + throw new Error(`Token refresh failed: ${response.status}, ${errorText}`); 158 + } 159 + 160 + const tokenData = await response.json(); 161 + 162 + // Get any nonce from the response headers 163 + const responseNonce = response.headers.get('DPoP-Nonce'); 164 + 165 + // Return the new tokens and nonce 166 + return { 167 + accessToken: tokenData.access_token, 168 + refreshToken: tokenData.refresh_token || refreshToken, // Use the new refresh token if provided 169 + dpopNonce: responseNonce || dpopNonce 170 + }; 171 + } catch (error) { 172 + console.error('Error refreshing access token:', error); 173 + throw error; 174 + } 175 + } 176 + 17 177 // Check if authentication is valid by making a simple request 18 178 export async function checkAuth( 19 179 accessToken: string, 20 180 keyPair: CryptoKeyPair, 21 - did: string, // Add DID parameter 181 + did: string, 22 182 dpopNonce: string | null = null, 23 - pdsEndpoint: string | null = null 183 + pdsEndpoint: string | null = null, 184 + refreshToken: string | null = null // Add refresh token parameter 24 185 ): Promise<boolean> { 25 186 try { 26 187 if (!pdsEndpoint) { ··· 31 192 if (!did) { 32 193 console.error('No DID provided for auth check'); 33 194 return false; 195 + } 196 + 197 + // First check if the token is expired by decoding it 198 + const tokenExpired = isTokenExpired(accessToken); 199 + if (tokenExpired && refreshToken) { 200 + console.log('Access token is expired, attempting to refresh...'); 201 + 202 + try { 203 + // Try to refresh the token 204 + const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } = 205 + await refreshAccessToken(refreshToken, keyPair, pdsEndpoint); 206 + 207 + // Update tokens in localStorage 208 + localStorage.setItem('accessToken', newAccessToken); 209 + localStorage.setItem('refreshToken', newRefreshToken); 210 + if (newNonce) localStorage.setItem('dpopNonce', newNonce); 211 + 212 + console.log('Token refreshed successfully, retrying auth check with new token'); 213 + 214 + // Return the result of checkAuth with the new token 215 + return checkAuth(newAccessToken, keyPair, did, newNonce || null, pdsEndpoint, newRefreshToken); 216 + } catch (refreshError) { 217 + console.error('Token refresh failed:', refreshError); 218 + return false; 219 + } 34 220 } 35 221 36 222 console.log('Checking auth with PDS endpoint:', pdsEndpoint); ··· 88 274 if (nonce) { 89 275 console.log('Got nonce during auth check:', nonce); 90 276 // Try again with the nonce, but prevent infinite recursion 91 - return checkAuth(accessToken, keyPair, did, nonce, pdsEndpoint); 277 + return checkAuth(accessToken, keyPair, did, nonce, pdsEndpoint, refreshToken); 278 + } 279 + 280 + // If we have a refresh token, try to refresh the access token 281 + if (refreshToken && !tokenExpired) { // Only try this if we didn't already try above 282 + console.log('Auth failed with 401, attempting to refresh token...'); 283 + 284 + try { 285 + // Try to refresh the token 286 + const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } = 287 + await refreshAccessToken(refreshToken, keyPair, pdsEndpoint); 288 + 289 + // Update tokens in localStorage 290 + localStorage.setItem('accessToken', newAccessToken); 291 + localStorage.setItem('refreshToken', newRefreshToken); 292 + if (newNonce) localStorage.setItem('dpopNonce', newNonce); 293 + 294 + console.log('Token refreshed successfully, retrying auth check with new token'); 295 + 296 + // Return the result of checkAuth with the new token 297 + return checkAuth(newAccessToken, keyPair, did, newNonce || null, pdsEndpoint, newRefreshToken); 298 + } catch (refreshError) { 299 + console.error('Token refresh failed:', refreshError); 300 + } 92 301 } 93 302 } 94 303 ··· 329 538 emoji: string, 330 539 dpopNonce: string | null = null, 331 540 pdsEndpoint: string | null = null, 332 - retryCount: number = 0 // Add retry counter 541 + retryCount: number = 0, // Add retry counter 542 + refreshToken: string | null = null // Add refresh token parameter 333 543 ): Promise<any> { 334 544 // Safety check: prevent infinite recursion 335 545 if (retryCount >= 3) { ··· 418 628 emoji, 419 629 errorData.nonce, 420 630 pdsEndpoint, 421 - retryCount + 1 631 + retryCount + 1, 632 + refreshToken // Pass through the refresh token 422 633 ); 634 + } 635 + 636 + // Handle expired token with refresh 637 + if (response.status === 401 && refreshToken) { 638 + console.log('Authentication error (401), attempting token refresh...'); 639 + 640 + try { 641 + // Try to refresh the token 642 + const { accessToken: newAccessToken, refreshToken: newRefreshToken, dpopNonce: newNonce } = 643 + await refreshAccessToken(refreshToken, keyPair, pdsEndpoint || 'https://bsky.social'); 644 + 645 + // Update tokens in localStorage 646 + if (typeof localStorage !== 'undefined') { 647 + localStorage.setItem('accessToken', newAccessToken); 648 + localStorage.setItem('refreshToken', newRefreshToken); 649 + if (newNonce) localStorage.setItem('dpopNonce', newNonce); 650 + } 651 + 652 + console.log('Token refreshed successfully, retrying status creation'); 653 + 654 + // Retry with the new token 655 + return createFlushingStatus( 656 + newAccessToken, 657 + keyPair, 658 + did, 659 + statusText, 660 + emoji, 661 + newNonce || null, 662 + pdsEndpoint, 663 + retryCount + 1, 664 + newRefreshToken 665 + ); 666 + } catch (refreshError) { 667 + console.error('Token refresh failed:', refreshError); 668 + throw new Error('Authentication expired. Please log out and log in again.'); 669 + } 423 670 } 424 671 425 672 // For other errors, throw with more details