The smokesignal.events web application
at main 152 lines 4.3 kB view raw
1/** 2 * Authentication utilities for session management 3 * 4 * Provides session refresh functionality that should be called before 5 * making authenticated requests to AT Protocol services. 6 */ 7 8/** 9 * Session refresh error types 10 */ 11export class AuthRequiredError extends Error { 12 constructor(message = 'Authentication required') { 13 super(message) 14 this.name = 'AuthRequiredError' 15 } 16} 17 18export class SessionExpiredError extends Error { 19 constructor(message = 'Your session has expired. Please log in again.') { 20 super(message) 21 this.name = 'SessionExpiredError' 22 } 23} 24 25/** 26 * Refreshes the OAuth session token. 27 * 28 * This should be called before any request that uses AT Protocol OAuth tokens. 29 * The server will use the refresh token to obtain new access tokens if needed. 30 * 31 * @throws {AuthRequiredError} If no session exists 32 * @throws {SessionExpiredError} If the session cannot be refreshed 33 */ 34export async function refreshSession(): Promise<void> { 35 try { 36 const response = await fetch('/oauth/refresh', { 37 method: 'POST', 38 credentials: 'same-origin', 39 }) 40 41 if (!response.ok) { 42 if (response.status === 401) { 43 throw new AuthRequiredError() 44 } 45 // For other errors, we'll let them pass through silently 46 // The actual request will handle auth failures 47 console.warn('Session refresh returned non-OK status:', response.status) 48 } 49 } catch (e) { 50 if (e instanceof AuthRequiredError) throw e 51 // Network errors during refresh shouldn't block the request 52 // The actual request will handle auth failures 53 console.warn('Session refresh failed:', e) 54 } 55} 56 57/** 58 * Refreshes authentication before an authenticated request. 59 * Provides user-friendly error messages for session expiration. 60 * 61 * @throws {SessionExpiredError} If session has expired and cannot be refreshed 62 */ 63export async function refreshAuth(): Promise<void> { 64 try { 65 await refreshSession() 66 } catch (e) { 67 if (e instanceof AuthRequiredError) { 68 throw new SessionExpiredError() 69 } 70 // For other errors, let them pass - the actual request will handle it 71 console.warn('Auth refresh warning:', e) 72 } 73} 74 75/** 76 * Options for authenticated fetch requests 77 */ 78export interface AuthFetchOptions extends RequestInit { 79 /** 80 * Whether to skip session refresh before the request. 81 * Default: false (always refresh) 82 */ 83 skipRefresh?: boolean 84} 85 86/** 87 * Fetch wrapper that refreshes the session before making authenticated requests. 88 * 89 * Use this for any requests that require AT Protocol OAuth authentication. 90 * 91 * @param url - The URL to fetch 92 * @param options - Fetch options with optional skipRefresh flag 93 * @returns The fetch response 94 * @throws {SessionExpiredError} If authentication fails 95 */ 96export async function authFetch(url: string, options: AuthFetchOptions = {}): Promise<Response> { 97 const { skipRefresh = false, ...fetchOptions } = options 98 99 if (!skipRefresh) { 100 await refreshAuth() 101 } 102 103 const response = await fetch(url, { 104 ...fetchOptions, 105 credentials: 'same-origin', 106 }) 107 108 // Handle authentication failures 109 if (response.status === 401) { 110 throw new SessionExpiredError() 111 } 112 113 return response 114} 115 116/** 117 * JSON fetch helper that refreshes session and handles JSON parsing. 118 * 119 * @param url - The URL to fetch 120 * @param options - Fetch options 121 * @returns Parsed JSON response 122 * @throws {SessionExpiredError} If authentication fails 123 */ 124export async function authFetchJson<T>(url: string, options: AuthFetchOptions = {}): Promise<T> { 125 const response = await authFetch(url, options) 126 return response.json() as Promise<T> 127} 128 129/** 130 * POST JSON helper that refreshes session before posting. 131 * 132 * @param url - The URL to post to 133 * @param data - Data to send as JSON body 134 * @param options - Additional fetch options 135 * @returns The fetch response 136 * @throws {SessionExpiredError} If authentication fails 137 */ 138export async function authPostJson( 139 url: string, 140 data: unknown, 141 options: AuthFetchOptions = {} 142): Promise<Response> { 143 return authFetch(url, { 144 method: 'POST', 145 headers: { 146 'Content-Type': 'application/json', 147 ...((options.headers as Record<string, string>) || {}), 148 }, 149 body: JSON.stringify(data), 150 ...options, 151 }) 152}