your personal website on atproto - mirror
blento.app
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}