1import { Client } from "@atcute/client";
2import { Did } from "@atcute/lexicons";
3import { isHandle } from "@atcute/lexicons/syntax";
4import {
5 configureOAuth,
6 createAuthorizationUrl,
7 finalizeAuthorization,
8 getSession,
9 OAuthUserAgent,
10 resolveFromIdentity,
11 resolveFromService,
12 type Session,
13} from "@atcute/oauth-browser-client";
14import { createSignal, Show } from "solid-js";
15
16configureOAuth({
17 metadata: {
18 client_id: import.meta.env.VITE_OAUTH_CLIENT_ID,
19 redirect_uri: import.meta.env.VITE_OAUTH_REDIRECT_URL,
20 },
21});
22
23export const [agent, setAgent] = createSignal<OAuthUserAgent | undefined>();
24
25type Account = {
26 signedIn: boolean;
27 handle?: string;
28};
29
30export type Sessions = Record<string, Account>;
31
32const Login = () => {
33 const [notice, setNotice] = createSignal("");
34 const [loginInput, setLoginInput] = createSignal("");
35
36 const login = async (handle: string) => {
37 try {
38 setNotice("");
39 if (!handle) return;
40 let resolved;
41 if (!isHandle(handle)) {
42 setNotice(`Resolving your service...`);
43 resolved = await resolveFromService(handle);
44 } else {
45 setNotice(`Resolving your identity...`);
46 resolved = await resolveFromIdentity(handle);
47 }
48
49 setNotice(`Contacting your data server...`);
50 const authUrl = await createAuthorizationUrl({
51 scope: import.meta.env.VITE_OAUTH_SCOPE,
52 ...resolved,
53 });
54
55 setNotice(`Redirecting...`);
56 await new Promise((resolve) => setTimeout(resolve, 250));
57
58 location.assign(authUrl);
59 } catch (e) {
60 console.error(e);
61 setNotice(`${e}`);
62 }
63 };
64
65 return (
66 <form class="flex flex-col gap-y-2 px-1" onsubmit={(e) => e.preventDefault()}>
67 <label for="handle" class="hidden">
68 Add account
69 </label>
70 <div class="dark:bg-dark-100 dark:shadow-dark-700 flex grow items-center gap-2 rounded-lg border-[0.5px] border-neutral-300 bg-white px-2 shadow-xs focus-within:outline-[1px] focus-within:outline-neutral-600 dark:border-neutral-600 dark:focus-within:outline-neutral-400">
71 <label
72 for="handle"
73 class="iconify lucide--user-round-plus shrink-0 text-neutral-500 dark:text-neutral-400"
74 ></label>
75 <input
76 type="text"
77 spellcheck={false}
78 placeholder="user.bsky.social"
79 id="handle"
80 class="grow py-1 select-none placeholder:text-sm focus:outline-none"
81 onInput={(e) => setLoginInput(e.currentTarget.value)}
82 />
83 <button
84 onclick={() => login(loginInput())}
85 class="flex items-center rounded-lg p-1 hover:bg-neutral-100 active:bg-neutral-200 dark:hover:bg-neutral-600 dark:active:bg-neutral-500"
86 >
87 <span class="iconify lucide--log-in"></span>
88 </button>
89 </div>
90 <Show when={notice()}>
91 <div class="text-sm">{notice()}</div>
92 </Show>
93 </form>
94 );
95};
96
97const retrieveSession = async () => {
98 const init = async (): Promise<Session | undefined> => {
99 const params = new URLSearchParams(location.hash.slice(1));
100
101 if (params.has("state") && (params.has("code") || params.has("error"))) {
102 history.replaceState(null, "", location.pathname + location.search);
103
104 const session = await finalizeAuthorization(params);
105 const did = session.info.sub;
106
107 localStorage.setItem("lastSignedIn", did);
108
109 const sessions = localStorage.getItem("sessions");
110 const newSessions: Sessions = sessions ? JSON.parse(sessions) : { [did]: {} };
111 newSessions[did] = { signedIn: true };
112 localStorage.setItem("sessions", JSON.stringify(newSessions));
113 return session;
114 } else {
115 const lastSignedIn = localStorage.getItem("lastSignedIn");
116
117 if (lastSignedIn) {
118 const sessions = localStorage.getItem("sessions");
119 const newSessions: Sessions = sessions ? JSON.parse(sessions) : {};
120 try {
121 const session = await getSession(lastSignedIn as Did);
122 const rpc = new Client({ handler: new OAuthUserAgent(session) });
123 const res = await rpc.get("com.atproto.server.getSession");
124 newSessions[lastSignedIn].signedIn = true;
125 localStorage.setItem("sessions", JSON.stringify(newSessions));
126 if (!res.ok) throw res.data.error;
127 return session;
128 } catch (err) {
129 newSessions[lastSignedIn].signedIn = false;
130 localStorage.setItem("sessions", JSON.stringify(newSessions));
131 throw err;
132 }
133 }
134 }
135 };
136
137 const session = await init();
138
139 if (session) setAgent(new OAuthUserAgent(session));
140};
141
142export { Login, retrieveSession };