One-click backups for AT Protocol
1import { Agent } from "@atproto/api";
2import { ProfileViewDetailed } from "@atproto/api/dist/client/types/app/bsky/actor/defs";
3import { OAuthClient, type OAuthSession } from "@atproto/oauth-client";
4import { BrowserOAuthClient } from "@atproto/oauth-client-browser";
5import { Store } from "@tauri-apps/plugin-store";
6import {
7 createContext,
8 ReactNode,
9 useContext,
10 useEffect,
11 useState,
12} from "react";
13
14interface AuthContextType {
15 isLoading: boolean;
16 isAuthenticated: boolean;
17 profile: ProfileViewDetailed | null;
18 client: OAuthClient | null;
19 login: (session: OAuthSession) => Promise<void>;
20 logout: () => Promise<void>;
21 agent: Agent | null;
22}
23
24const AuthContext = createContext<AuthContextType | null>(null);
25
26export function AuthProvider({ children }: { children: ReactNode }) {
27 const [isLoading, setIsLoading] = useState(true);
28 const [profile, setProfile] = useState<ProfileViewDetailed | null>(null);
29 const [client, setClient] = useState<OAuthClient | null>(null);
30 const [agent, setAgent] = useState<Agent | null>(null);
31
32 // Initialize OAuth client
33 useEffect(() => {
34 const init = async () => {
35 setIsLoading(true);
36 try {
37 const client = await BrowserOAuthClient.load({
38 clientId: "https://atproto-backup.pages.dev/client_metadata.json",
39 handleResolver: "https://bsky.social",
40 });
41
42 setClient(client);
43
44 // Try to restore existing session from storage
45 try {
46 //@ts-expect-error
47 const result: undefined | { session: OAuthSession; state?: string } =
48 await client.init();
49
50 if (result) {
51 const { session, state } = result;
52 if (state != null) {
53 console.log(
54 `${session.sub} was successfully authenticated (state: ${state})`
55 );
56 } else {
57 console.log(`${session.sub} was restored (last active session)`);
58 }
59 await login(session);
60 } else {
61 const store = await Store.load("store.json", { autoSave: true });
62 const sub = (await store.get("session")) as string;
63 if (sub) {
64 const session = await client.restore(sub);
65 await login(session);
66 return;
67 }
68 }
69 } catch (error) {
70 console.error("Failed to restore session:", error);
71 }
72 } catch (error) {
73 console.error("Failed to initialize auth:", error);
74 } finally {
75 setIsLoading(false);
76 }
77 };
78
79 init();
80 }, []);
81
82 const login = async (session: OAuthSession) => {
83 try {
84 const store = await Store.load("store.json", { autoSave: true });
85 store.set("session", session.did);
86 const agent = new Agent(session);
87 setAgent(agent);
88 setIsLoading(true);
89
90 const actor = await agent.getProfile({ actor: session.did });
91 setProfile(actor.data);
92 } catch (error) {
93 console.error("Login failed:", error);
94 throw error;
95 } finally {
96 setIsLoading(false);
97 }
98 };
99
100 const logout = async () => {
101 try {
102 if (client && profile) {
103 // Revoke the session and clear storage
104 await client.revoke(profile.did);
105 const store = await Store.load("store.json", { autoSave: true });
106 // probably unnecessary
107 await store.clear();
108 setProfile(null);
109 }
110 } catch (error) {
111 console.error("Logout failed:", error);
112 throw error;
113 }
114 };
115
116 return (
117 <AuthContext.Provider
118 value={{
119 isLoading,
120 isAuthenticated: !!profile,
121 profile,
122 client,
123 login,
124 logout,
125 agent,
126 }}
127 >
128 {children}
129 </AuthContext.Provider>
130 );
131}
132export function useAuth() {
133 const context = useContext(AuthContext);
134 if (!context) {
135 throw new Error("useAuth must be used within an AuthProvider");
136 }
137 return context;
138}