[READ-ONLY] Mirror of https://github.com/flo-bit/svelte-cloudflare-statusphere. statusphere.atmo.tools
atproto cloudflare-workers oauth svelte
0

Configure Feed

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

svelte-cloudflare-statusphere / src / lib / oauth / auth.svelte.ts
4.6 kB 182 lines
1import { 2 configureOAuth, 3 createAuthorizationUrl, 4 finalizeAuthorization, 5 resolveFromIdentity, 6 type Session, 7 OAuthUserAgent, 8 getSession 9} from '@atcute/oauth-browser-client'; 10import { dev } from '$app/environment'; 11import { XRPC } from '@atcute/client'; 12import { metadata } from './const'; 13 14export const client = $state({ 15 agent: null as OAuthUserAgent | null, 16 session: null as Session | null, 17 rpc: null as XRPC | null, 18 profile: null as { 19 handle: string; 20 did: string; 21 createdAt: string; 22 description?: string; 23 displayName?: string; 24 banner?: string; 25 avatar?: string; 26 followersCount?: number; 27 followsCount?: number; 28 postsCount?: number; 29 } | null, 30 isInitializing: true, 31 isLoggedIn: false 32}); 33 34export async function initClient() { 35 client.isInitializing = true; 36 37 const clientId = dev 38 ? `http://localhost` + 39 `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179')}` + 40 `&scope=${encodeURIComponent(metadata.scope)}` 41 : metadata.client_id; 42 43 configureOAuth({ 44 metadata: { 45 client_id: clientId, 46 redirect_uri: `${dev ? 'http://127.0.0.1:5179' : metadata.redirect_uris[0]}` 47 } 48 }); 49 50 const params = new URLSearchParams(location.hash.slice(1)); 51 52 const did = localStorage.getItem('last-login') ?? undefined; 53 54 if (params.size > 0) { 55 await finalizeLogin(params, did); 56 } else if (did) { 57 await resumeSession(did); 58 } 59 60 client.isInitializing = false; 61} 62 63export async function login(handle: string) { 64 if (handle.startsWith('did:')) { 65 if (handle.length > 5) await authorizationFlow(handle); 66 else throw new Error('DID must be at least 6 characters'); 67 } else if (handle.includes('.') && handle.length > 3) { 68 const processed = handle.startsWith('@') ? handle.slice(1) : handle; 69 if (processed.length > 3) await authorizationFlow(processed); 70 else throw new Error('Handle must be at least 4 characters'); 71 } else if (handle.length > 3) { 72 const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social'; 73 await authorizationFlow(processed); 74 } else { 75 throw new Error('Please provide a valid handle, DID, or PDS URL'); 76 } 77} 78 79export async function logout() { 80 const currentAgent = client.agent; 81 if (currentAgent) { 82 const did = currentAgent.session.info.sub; 83 84 localStorage.removeItem('last-login'); 85 localStorage.removeItem(`profile-${did}`); 86 87 await currentAgent.signOut(); 88 client.session = null; 89 client.agent = null; 90 client.profile = null; 91 92 client.isLoggedIn = false; 93 } else { 94 throw new Error('Not signed in'); 95 } 96} 97 98async function finalizeLogin(params: URLSearchParams, did?: string) { 99 try { 100 history.replaceState(null, '', location.pathname + location.search); 101 102 const session = await finalizeAuthorization(params); 103 client.session = session; 104 105 setAgentAndXRPC(session); 106 localStorage.setItem('last-login', session.info.sub); 107 108 await loadProfile(session.info.sub); 109 110 client.isLoggedIn = true; 111 } catch (error) { 112 console.error('error finalizing login', error); 113 if (did) { 114 await resumeSession(did); 115 } 116 } 117} 118 119async function resumeSession(did: string) { 120 try { 121 const session = await getSession(did as `did:${string}`, { allowStale: true }); 122 client.session = session; 123 124 setAgentAndXRPC(session); 125 126 await loadProfile(session.info.sub); 127 128 client.isLoggedIn = true; 129 } catch (error) { 130 console.error('error resuming session', error); 131 } 132} 133 134function setAgentAndXRPC(session: Session) { 135 client.agent = new OAuthUserAgent(session); 136 137 client.rpc = new XRPC({ handler: client.agent }); 138} 139 140async function loadProfile(actor: string) { 141 // check if profile is already loaded in local storage 142 const profile = localStorage.getItem(`profile-${actor}`); 143 if (profile) { 144 console.log('loading profile from local storage'); 145 client.profile = JSON.parse(profile); 146 return; 147 } 148 149 console.log('loading profile from server'); 150 const response = await client.rpc?.request({ 151 type: 'get', 152 nsid: 'app.bsky.actor.getProfile', 153 params: { actor } 154 }); 155 156 if (response) { 157 client.profile = response.data; 158 localStorage.setItem(`profile-${actor}`, JSON.stringify(response.data)); 159 } 160} 161 162async function authorizationFlow(input: string) { 163 const { identity, metadata: meta } = await resolveFromIdentity(input); 164 165 const authUrl = await createAuthorizationUrl({ 166 metadata: meta, 167 identity: identity, 168 scope: metadata.scope 169 }); 170 171 await new Promise((resolve) => setTimeout(resolve, 200)); 172 173 window.location.assign(authUrl); 174 175 await new Promise((_resolve, reject) => { 176 const listener = () => { 177 reject(new Error(`user aborted the login request`)); 178 }; 179 180 window.addEventListener('pageshow', listener, { once: true }); 181 }); 182}