The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
at main 176 lines 5.3 kB view raw
1import { BrowserOAuthClient } from '@atproto/oauth-client-browser' 2 3// Client metadata for the OAuth client 4const CLIENT_METADATA = { 5 "client_id": "https://flushes.app/oauth-client-metadata.json", 6 "application_type": "web" as const, 7 "client_name": "Flushes", 8 "client_uri": "https://flushes.app", 9 "logo_uri": "https://flushes.app/logo.png", 10 "tos_uri": "https://flushes.app/terms", 11 "policy_uri": "https://flushes.app/privacy", 12 "dpop_bound_access_tokens": true, 13 "grant_types": ["authorization_code", "refresh_token"] as const, 14 "redirect_uris": ["https://flushes.app/auth/callback"] as const, 15 "response_types": ["code"] as const, 16 "scope": "atproto transition:generic", 17 "token_endpoint_auth_method": "none" as const 18} 19 20// Lazy OAuth client - only initialize on client side 21let _oauthClient: BrowserOAuthClient | null = null 22 23// Get or create the OAuth client instance - client-side only 24function getOAuthClient(): BrowserOAuthClient { 25 // Ensure we're on the client side 26 if (typeof window === 'undefined') { 27 throw new Error('OAuth client can only be used on the client side') 28 } 29 30 if (!_oauthClient) { 31 _oauthClient = new BrowserOAuthClient({ 32 clientMetadata: CLIENT_METADATA as any, 33 handleResolver: 'https://public.api.bsky.app', 34 responseMode: 'fragment' 35 }) 36 } 37 38 return _oauthClient 39} 40 41// Export the getter function instead of the instance 42export const oauthClient = { 43 get instance() { 44 return getOAuthClient() 45 } 46} 47 48// Initialize the client - this should be called once when the app loads 49export async function initializeOAuthClient() { 50 // Only run on client side 51 if (typeof window === 'undefined') { 52 console.log('Skipping OAuth client initialization on server side') 53 return null 54 } 55 56 try { 57 const client = getOAuthClient() 58 const result = await client.init() 59 60 if (result) { 61 const { session } = result 62 const state = 'state' in result ? result.state : null 63 console.log(`OAuth client initialized with session for ${session.sub}`) 64 65 if (state) { 66 console.log(`User successfully authenticated with state: ${state}`) 67 } else { 68 console.log(`Restored previous session`) 69 } 70 71 return { session, state } 72 } 73 74 console.log('OAuth client initialized without existing session') 75 return null 76 } catch (error) { 77 console.error('Failed to initialize OAuth client:', error) 78 throw error 79 } 80} 81 82// Sign in with handle/DID 83export async function signIn(handle: string, options?: { 84 state?: string 85 signal?: AbortSignal 86}) { 87 // Only run on client side 88 if (typeof window === 'undefined') { 89 throw new Error('Sign in can only be called on the client side') 90 } 91 92 try { 93 console.log(`Initiating OAuth flow for ${handle}`) 94 95 const client = getOAuthClient() 96 await client.signIn(handle, { 97 state: options?.state || `signin-${Date.now()}`, 98 signal: options?.signal 99 }) 100 101 // This will never resolve as the user gets redirected 102 } catch (error) { 103 console.error('OAuth sign in failed:', error) 104 throw error 105 } 106} 107 108// Restore a specific session by DID 109export async function restoreSession(did: string) { 110 // Only run on client side 111 if (typeof window === 'undefined') { 112 throw new Error('Restore session can only be called on the client side') 113 } 114 115 try { 116 console.log(`Restoring session for ${did}`) 117 const client = getOAuthClient() 118 const session = await client.restore(did) 119 console.log(`Successfully restored session for ${session.sub}`) 120 return session 121 } catch (error) { 122 console.error(`Failed to restore session for ${did}:`, error) 123 throw error 124 } 125} 126 127// Sign out the current session 128export async function signOut() { 129 // Only run on client side 130 if (typeof window === 'undefined') { 131 throw new Error('Sign out can only be called on the client side') 132 } 133 134 try { 135 console.log('Signing out user') 136 137 // Clear any remaining localStorage items from the old implementation 138 if (typeof localStorage !== 'undefined') { 139 localStorage.removeItem('accessToken') 140 localStorage.removeItem('refreshToken') 141 localStorage.removeItem('did') 142 localStorage.removeItem('handle') 143 localStorage.removeItem('keyPair') 144 localStorage.removeItem('dpopNonce') 145 localStorage.removeItem('pdsEndpoint') 146 localStorage.removeItem('bsky_auth_pdsEndpoint') 147 } 148 149 // The OAuth client manages its own storage 150 // We don't have direct access to clear it, but it will handle session cleanup 151 console.log('User signed out') 152 } catch (error) { 153 console.error('Error during sign out:', error) 154 throw error 155 } 156} 157 158// Event listener for session deletion/invalidation 159export function onSessionDeleted(callback: (event: { sub: string, cause: any }) => void) { 160 // Only run on client side 161 if (typeof window === 'undefined') { 162 console.log('Skipping session deleted listener setup on server side') 163 return 164 } 165 166 try { 167 const client = getOAuthClient() 168 client.addEventListener('deleted', (event: any) => { 169 const { sub, cause } = event.detail 170 console.error(`Session for ${sub} was invalidated:`, cause) 171 callback({ sub, cause }) 172 }) 173 } catch (error) { 174 console.error('Failed to set up session deleted listener:', error) 175 } 176}