This repository has no description
0

Configure Feed

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

formatting

+46 -23
+1 -1
docs/examples/next-js-ssr-component.md
··· 5 5 6 6 export default async function SSRProfilePage() { 7 7 const apiClient = await createServerApiClient(); 8 - 8 + 9 9 let profile; 10 10 let error; 11 11
+38 -18
docs/token-refreshing.md
··· 30 30 } 31 31 32 32 export interface TokenRefresher { 33 - refreshTokens(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }>; 33 + refreshTokens( 34 + refreshToken: string, 35 + ): Promise<{ accessToken: string; refreshToken: string }>; 34 36 } 35 37 ``` 36 38 ··· 73 75 74 76 // Start refresh process 75 77 this.isRefreshing = true; 76 - 78 + 77 79 try { 78 80 // Use existing refresh promise or create new one 79 81 if (!this.refreshPromise) { ··· 103 105 // Refresh failed, reject all queued requests 104 106 const queuedRequests = [...this.failedRequestsQueue]; 105 107 this.failedRequestsQueue = []; 106 - 108 + 107 109 const refreshError = new Error('Token refresh failed'); 108 110 queuedRequests.forEach(({ reject }) => reject(refreshError)); 109 - 111 + 110 112 throw refreshError; 111 113 } 112 114 } finally { ··· 121 123 122 124 try { 123 125 const newTokens = await this.refresher.refreshTokens(refreshToken); 124 - await this.storage.setTokens(newTokens.accessToken, newTokens.refreshToken); 126 + await this.storage.setTokens( 127 + newTokens.accessToken, 128 + newTokens.refreshToken, 129 + ); 125 130 return true; 126 131 } catch (error) { 127 132 console.error('Token refresh failed:', error); ··· 143 148 if (typeof window === 'undefined') { 144 149 return { accessToken: null, refreshToken: null }; 145 150 } 146 - 151 + 147 152 return { 148 153 accessToken: localStorage.getItem('accessToken'), 149 154 refreshToken: localStorage.getItem('refreshToken'), ··· 152 157 153 158 async setTokens(accessToken: string, refreshToken: string): Promise<void> { 154 159 if (typeof window === 'undefined') return; 155 - 160 + 156 161 localStorage.setItem('accessToken', accessToken); 157 162 localStorage.setItem('refreshToken', refreshToken); 158 - 163 + 159 164 // Sync with server cookies 160 165 try { 161 166 await fetch('/api/auth/sync', { ··· 210 215 export class ApiTokenRefresher implements TokenRefresher { 211 216 constructor(private baseUrl: string) {} 212 217 213 - async refreshTokens(refreshToken: string): Promise<{ accessToken: string; refreshToken: string }> { 218 + async refreshTokens( 219 + refreshToken: string, 220 + ): Promise<{ accessToken: string; refreshToken: string }> { 214 221 const response = await fetch(`${this.baseUrl}/api/auth/refresh`, { 215 222 method: 'POST', 216 223 headers: { 'Content-Type': 'application/json' }, ··· 260 267 headers, 261 268 }; 262 269 263 - if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { 270 + if ( 271 + data && 272 + (method === 'POST' || method === 'PUT' || method === 'PATCH') 273 + ) { 264 274 config.body = JSON.stringify(data); 265 275 } 266 276 ··· 317 327 export const createClientTokenManager = () => { 318 328 const storage = new ClientTokenStorage(); 319 329 const refresher = new ApiTokenRefresher( 320 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000' 330 + process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 321 331 ); 322 332 return new TokenManager(storage, refresher); 323 333 }; ··· 379 389 const tokenManager = useMemo(() => createClientTokenManager(), []); 380 390 381 391 // Helper function to check if a JWT token is expired or will expire soon 382 - const isTokenExpired = (token: string, bufferMinutes: number = 5): boolean => { 392 + const isTokenExpired = ( 393 + token: string, 394 + bufferMinutes: number = 5, 395 + ): boolean => { 383 396 if (!token) return true; 384 397 385 398 try { 386 399 const payload = JSON.parse(atob(token.split('.')[1])); 387 400 const expiry = payload.exp * 1000; // Convert to milliseconds 388 401 const bufferTime = bufferMinutes * 60 * 1000; // Buffer in milliseconds 389 - return Date.now() >= (expiry - bufferTime); 402 + return Date.now() >= expiry - bufferTime; 390 403 } catch (e) { 391 404 return true; 392 405 } ··· 406 419 407 420 // Check immediately on mount 408 421 checkAndRefreshToken(); 409 - 422 + 410 423 // Then check every 5 minutes 411 424 const interval = setInterval(checkAndRefreshToken, 5 * 60 * 1000); 412 425 ··· 427 440 - Server-side rendering scenarios 428 441 429 442 The TokenManager automatically: 443 + 430 444 1. **Detects auth errors** (401/403) from API responses 431 445 2. **Refreshes tokens** using the refresh token 432 446 3. **Retries the original request** with fresh tokens ··· 477 491 478 492 export default async function SSRProfilePage() { 479 493 const apiClient = await createServerApiClient(); 480 - 494 + 481 495 let profile; 482 496 let error; 483 497 ··· 502 516 ✅ **Race Condition Safe**: TokenManager prevents concurrent refreshes 503 517 ✅ **Automatic Recovery**: Failed requests are automatically retried after refresh 504 518 ✅ **Request Queuing**: Multiple concurrent requests handled gracefully 505 - ✅ **Server-Side Support**: Works in SSR with cookie-based tokens 519 + ✅ **Server-Side Support**: Works in SSR with cookie-based tokens 506 520 507 521 ## Server-Side Token Refresh Limitation 508 522 509 523 **Current Approach**: Server components use read-only token access from cookies. If tokens are expired, the request will fail gracefully. 510 524 511 525 **Why This Works**: 526 + 512 527 - Proactive refresh on client-side prevents most expiration scenarios 513 528 - Server components typically render fresh on each request 514 529 - Failed server requests degrade gracefully (show error states) 515 530 - Client-side components can still refresh and retry 516 531 517 532 **Future Enhancements** (if needed): 533 + 518 534 - Add middleware to check token expiry and redirect to refresh endpoint 519 535 - Implement server-side refresh with secure cookie updates 520 536 - Use hybrid approach with client-side refresh signals ··· 527 543 - **Refresh Token Lifetime**: 7 days (server-configured) 528 544 529 545 This means: 546 + 530 547 - **99% of cases**: Tokens refreshed proactively at 10 minutes (before 15-minute expiry) 531 548 - **1% of cases**: Reactive refresh handles edge cases when proactive refresh missed 532 549 - **Zero failed requests**: All auth errors automatically trigger refresh and retry ··· 569 586 570 587 ```typescript 571 588 // Multiple API calls happen when token is expired 572 - apiClient.getProfile(); // Triggers refresh 573 - apiClient.getCards(); // Queued, waits for refresh 589 + apiClient.getProfile(); // Triggers refresh 590 + apiClient.getCards(); // Queued, waits for refresh 574 591 apiClient.getCollections(); // Queued, waits for refresh 575 592 576 593 // After refresh completes: ··· 581 598 ## Monitoring 582 599 583 600 Consider tracking these metrics: 601 + 584 602 - **Proactive refresh frequency** (should be regular, every ~5 minutes) 585 603 - **Reactive refresh frequency** (should be rare, <1% of API calls) 586 604 - **Failed refresh attempts** (should investigate if >0.1%) ··· 593 611 ### From Old Approach 594 612 595 613 **Before:** 614 + 596 615 ```typescript 597 616 const apiClient = new ApiClient( 598 617 baseUrl, ··· 602 621 ``` 603 622 604 623 **After:** 624 + 605 625 ```typescript 606 626 import { apiClient } from '@/api-client'; 607 627 // Just use the default instance - no configuration needed!
+4 -1
src/webapp/api-client/clients/BaseClient.ts
··· 29 29 headers, 30 30 }; 31 31 32 - if (data && (method === 'POST' || method === 'PUT' || method === 'PATCH')) { 32 + if ( 33 + data && 34 + (method === 'POST' || method === 'PUT' || method === 'PATCH') 35 + ) { 33 36 config.body = JSON.stringify(data); 34 37 } 35 38
+3 -3
src/webapp/services/storage/ClientTokenStorage.ts
··· 5 5 if (typeof window === 'undefined') { 6 6 return { accessToken: null, refreshToken: null }; 7 7 } 8 - 8 + 9 9 return { 10 10 accessToken: localStorage.getItem('accessToken'), 11 11 refreshToken: localStorage.getItem('refreshToken'), ··· 14 14 15 15 async setTokens(accessToken: string, refreshToken: string): Promise<void> { 16 16 if (typeof window === 'undefined') return; 17 - 17 + 18 18 localStorage.setItem('accessToken', accessToken); 19 19 localStorage.setItem('refreshToken', refreshToken); 20 - 20 + 21 21 // Sync with server cookies 22 22 try { 23 23 await fetch('/api/auth/sync', {