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