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