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