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