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}