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