A social knowledge tool for researchers built on ATProto
at development 118 lines 3.2 kB view raw
1export interface AuthTokens { 2 accessToken: string | null; 3 refreshToken: string | null; 4} 5 6export interface TokenStorage { 7 getTokens(): AuthTokens; 8 setTokens(accessToken: string, refreshToken: string): Promise<void>; 9 clearTokens(): void; 10} 11 12export interface TokenRefresher { 13 refreshTokens( 14 refreshToken: string, 15 ): Promise<{ accessToken: string; refreshToken: string }>; 16} 17 18export class TokenManager { 19 private isRefreshing = false; 20 private refreshPromise: Promise<boolean> | null = null; 21 private failedRequestsQueue: Array<{ 22 resolve: (value: any) => void; 23 reject: (error: any) => void; 24 request: () => Promise<any>; 25 }> = []; 26 27 constructor( 28 private storage: TokenStorage, 29 private refresher: TokenRefresher, 30 ) {} 31 32 async getAccessToken(): Promise<string | null> { 33 const { accessToken } = this.storage.getTokens(); 34 return accessToken; 35 } 36 37 async getRefreshToken(): Promise<string | null> { 38 const { refreshToken } = this.storage.getTokens(); 39 return refreshToken; 40 } 41 42 async handleAuthError<T>(originalRequest: () => Promise<T>): Promise<T> { 43 // If already refreshing, queue this request 44 if (this.isRefreshing) { 45 return new Promise((resolve, reject) => { 46 this.failedRequestsQueue.push({ 47 resolve, 48 reject, 49 request: originalRequest, 50 }); 51 }); 52 } 53 54 // Start refresh process 55 this.isRefreshing = true; 56 57 try { 58 // Use existing refresh promise or create new one 59 if (!this.refreshPromise) { 60 this.refreshPromise = this.performRefresh(); 61 } 62 63 const refreshSuccess = await this.refreshPromise; 64 65 if (refreshSuccess) { 66 // Process queued requests 67 const queuedRequests = [...this.failedRequestsQueue]; 68 this.failedRequestsQueue = []; 69 70 // Retry all queued requests 71 queuedRequests.forEach(async ({ resolve, reject, request }) => { 72 try { 73 const result = await request(); 74 resolve(result); 75 } catch (error) { 76 reject(error); 77 } 78 }); 79 80 // Retry original request 81 return await originalRequest(); 82 } else { 83 // Refresh failed, reject all queued requests 84 const queuedRequests = [...this.failedRequestsQueue]; 85 this.failedRequestsQueue = []; 86 87 const refreshError = new Error('Token refresh failed'); 88 queuedRequests.forEach(({ reject }) => reject(refreshError)); 89 90 if (typeof window !== 'undefined') { 91 window.location.href = '/login'; 92 } 93 throw refreshError; 94 } 95 } finally { 96 this.isRefreshing = false; 97 this.refreshPromise = null; 98 } 99 } 100 101 private async performRefresh(): Promise<boolean> { 102 const { refreshToken } = this.storage.getTokens(); 103 if (!refreshToken) return false; 104 105 try { 106 const newTokens = await this.refresher.refreshTokens(refreshToken); 107 await this.storage.setTokens( 108 newTokens.accessToken, 109 newTokens.refreshToken, 110 ); 111 return true; 112 } catch (error) { 113 console.error('Token refresh failed:', error); 114 this.storage.clearTokens(); 115 return false; 116 } 117 } 118}