forked from
atpota.to/flushes.app
The 1st decentralized social network for sharing when you're on the toilet. Post a "flush" today! Powered by the AT Protocol.
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}