your personal website on atproto - mirror blento.app
at floating-button 228 lines 6.2 kB view raw
1import { 2 configureOAuth, 3 createAuthorizationUrl, 4 finalizeAuthorization, 5 OAuthUserAgent, 6 getSession, 7 deleteStoredSession 8} from '@atcute/oauth-browser-client'; 9import { AppBskyActorDefs } from '@atcute/bluesky'; 10import { 11 CompositeDidDocumentResolver, 12 CompositeHandleResolver, 13 DohJsonHandleResolver, 14 LocalActorResolver, 15 PlcDidDocumentResolver, 16 WebDidDocumentResolver, 17 WellKnownHandleResolver 18} from '@atcute/identity-resolver'; 19import { Client } from '@atcute/client'; 20import type { ActorIdentifier, Did } from '@atcute/lexicons'; 21 22import { dev } from '$app/environment'; 23import { replaceState } from '$app/navigation'; 24 25import { metadata } from './metadata'; 26import { getDetailedProfile } from './methods'; 27import { signUpPDS } from './settings'; 28import { SvelteURLSearchParams } from 'svelte/reactivity'; 29 30export const user = $state({ 31 agent: null as OAuthUserAgent | null, 32 client: null as Client | null, 33 profile: null as AppBskyActorDefs.ProfileViewDetailed | null | undefined, 34 isInitializing: true, 35 isLoggedIn: false, 36 did: undefined as Did | undefined 37}); 38 39export async function initClient() { 40 user.isInitializing = true; 41 42 const clientId = dev 43 ? `http://localhost` + 44 `?redirect_uri=${encodeURIComponent('http://127.0.0.1:5179/oauth/callback')}` + 45 `&scope=${encodeURIComponent(metadata.scope)}` 46 : metadata.client_id; 47 48 const handleResolver = new CompositeHandleResolver({ 49 methods: { 50 dns: new DohJsonHandleResolver({ dohUrl: 'https://mozilla.cloudflare-dns.com/dns-query' }), 51 http: new WellKnownHandleResolver() 52 } 53 }); 54 55 configureOAuth({ 56 metadata: { 57 client_id: clientId, 58 redirect_uri: dev ? 'http://127.0.0.1:5179/oauth/callback' : metadata.redirect_uris[0] 59 }, 60 identityResolver: new LocalActorResolver({ 61 handleResolver: handleResolver, 62 didDocumentResolver: new CompositeDidDocumentResolver({ 63 methods: { 64 plc: new PlcDidDocumentResolver(), 65 web: new WebDidDocumentResolver() 66 } 67 }) 68 }) 69 }); 70 71 const params = new SvelteURLSearchParams(location.hash.slice(1)); 72 73 const did = (localStorage.getItem('current-login') as Did) ?? undefined; 74 75 if (params.size > 0) { 76 await finalizeLogin(params, did); 77 } else if (did) { 78 await resumeSession(did); 79 } 80 81 user.isInitializing = false; 82} 83 84export async function login(handle: ActorIdentifier) { 85 console.log('login in with', handle); 86 if (handle.startsWith('did:')) { 87 if (handle.length < 6) throw new Error('DID must be at least 6 characters'); 88 89 await startAuthorization(handle as ActorIdentifier); 90 } else if (handle.includes('.') && handle.length > 3) { 91 const processed = handle.startsWith('@') ? handle.slice(1) : handle; 92 if (processed.length < 4) throw new Error('Handle must be at least 4 characters'); 93 94 await startAuthorization(processed as ActorIdentifier); 95 } else if (handle.length > 3) { 96 const processed = (handle.startsWith('@') ? handle.slice(1) : handle) + '.bsky.social'; 97 await startAuthorization(processed as ActorIdentifier); 98 } else { 99 throw new Error('Please provide a valid handle or DID.'); 100 } 101} 102 103export async function signup() { 104 await startAuthorization(); 105} 106 107async function startAuthorization(identity?: ActorIdentifier) { 108 const authUrl = await createAuthorizationUrl({ 109 target: identity 110 ? { type: 'account', identifier: identity } 111 : { type: 'pds', serviceUrl: signUpPDS }, 112 // @ts-expect-error - new stuff 113 prompt: identity ? undefined : 'create', 114 scope: metadata.scope 115 }); 116 117 // let browser persist local storage 118 await new Promise((resolve) => setTimeout(resolve, 200)); 119 120 window.location.assign(authUrl); 121 122 await new Promise((_resolve, reject) => { 123 const listener = () => { 124 reject(new Error(`user aborted the login request`)); 125 }; 126 127 window.addEventListener('pageshow', listener, { once: true }); 128 }); 129} 130 131export async function logout() { 132 const currentAgent = user.agent; 133 if (currentAgent) { 134 const did = currentAgent.session.info.sub; 135 136 localStorage.removeItem('current-login'); 137 localStorage.removeItem(`profile-${did}`); 138 139 try { 140 await currentAgent.signOut(); 141 } catch { 142 deleteStoredSession(did); 143 } 144 145 user.agent = null; 146 user.profile = null; 147 user.isLoggedIn = false; 148 } else { 149 console.error('trying to logout, but user not signed in'); 150 return false; 151 } 152} 153 154async function finalizeLogin(params: URLSearchParams, did?: Did) { 155 try { 156 const { session } = await finalizeAuthorization(params); 157 replaceState(location.pathname + location.search, {}); 158 159 user.agent = new OAuthUserAgent(session); 160 user.did = session.info.sub; 161 user.client = new Client({ handler: user.agent }); 162 163 localStorage.setItem('current-login', session.info.sub); 164 165 await loadProfile(session.info.sub); 166 167 user.isLoggedIn = true; 168 169 try { 170 if (!user.profile) return; 171 const recentLogins = JSON.parse(localStorage.getItem('recent-logins') || '{}'); 172 173 recentLogins[session.info.sub] = user.profile; 174 175 localStorage.setItem('recent-logins', JSON.stringify(recentLogins)); 176 } catch { 177 console.log('failed to save to recent logins'); 178 } 179 } catch (error) { 180 console.error('error finalizing login', error); 181 if (did) { 182 await resumeSession(did); 183 } 184 } 185} 186 187async function resumeSession(did: Did) { 188 try { 189 const session = await getSession(did, { allowStale: true }); 190 191 if (session.token.expires_at && session.token.expires_at < Date.now()) { 192 throw Error('session expired'); 193 } 194 195 if (session.token.scope !== metadata.scope) { 196 throw Error('scope changed, signing out!'); 197 } 198 199 user.agent = new OAuthUserAgent(session); 200 user.did = session.info.sub; 201 user.client = new Client({ handler: user.agent }); 202 203 await loadProfile(session.info.sub); 204 205 user.isLoggedIn = true; 206 } catch (error) { 207 console.error('error resuming session', error); 208 deleteStoredSession(did); 209 } 210} 211 212async function loadProfile(actor: Did) { 213 // check if profile is already loaded in local storage 214 const profile = localStorage.getItem(`profile-${actor}`); 215 if (profile) { 216 try { 217 user.profile = JSON.parse(profile); 218 return; 219 } catch { 220 console.error('error loading profile from local storage'); 221 } 222 } 223 224 const response = await getDetailedProfile(); 225 226 user.profile = response; 227 localStorage.setItem(`profile-${actor}`, JSON.stringify(response)); 228}