an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 6.5 kB view raw
1import { Agent, AtpAgent, type AtpSessionData } from "@atproto/api"; 2import { 3 type OAuthSession, 4 TokenInvalidError, 5 TokenRefreshError, 6 TokenRevokedError, 7} from "@atproto/oauth-client-browser"; 8import { useAtom } from "jotai"; 9import React, { 10 createContext, 11 use, 12 useCallback, 13 useEffect, 14 useState, 15} from "react"; 16 17import { quickAuthAtom } from "~/utils/atoms"; 18 19import { oauthClient } from "../utils/oauthClient"; 20 21type AuthStatus = "loading" | "signedIn" | "signedOut"; 22type AuthMethod = "password" | "oauth" | null; 23 24interface AuthContextValue { 25 agent: Agent | null; 26 status: AuthStatus; 27 authMethod: AuthMethod; 28 loginWithPassword: ( 29 user: string, 30 password: string, 31 service?: string, 32 ) => Promise<void>; 33 loginWithOAuth: (handleOrPdsUrl: string) => Promise<void>; 34 logout: () => Promise<void>; 35} 36 37const AuthContext = createContext<AuthContextValue>({} as AuthContextValue); 38 39export const UnifiedAuthProvider = ({ 40 children, 41}: { 42 children: React.ReactNode; 43}) => { 44 const [agent, setAgent] = useState<Agent | null>(null); 45 const [status, setStatus] = useState<AuthStatus>("loading"); 46 const [authMethod, setAuthMethod] = useState<AuthMethod>(null); 47 const [oauthSession, setOauthSession] = useState<OAuthSession | null>(null); 48 const [quickAuth, setQuickAuth] = useAtom(quickAuthAtom); 49 50 const initialize = useCallback(async () => { 51 try { 52 const oauthResult = await oauthClient.init(); 53 if (oauthResult) { 54 // /*mass comment*/ console.log("OAuth session restored."); 55 const apiAgent = new Agent(oauthResult.session); 56 setAgent(apiAgent); 57 setOauthSession(oauthResult.session); 58 setAuthMethod("oauth"); 59 setStatus("signedIn"); 60 setQuickAuth(apiAgent?.did || null); 61 return; 62 } 63 } catch (e) { 64 console.error("OAuth init failed, checking password session.", e); 65 if (!quickAuth) { 66 // quickAuth restoration. if last used method is oauth we immediately call for oauth redo 67 // (and set a persistent atom somewhere to not retry again if it failed) 68 } 69 } 70 71 try { 72 const service = localStorage.getItem("service"); 73 const sessionString = localStorage.getItem("sess"); 74 75 if (service && sessionString) { 76 // /*mass comment*/ console.log("Resuming password-based session using AtpAgent..."); 77 const apiAgent = new AtpAgent({ service }); 78 const session: AtpSessionData = JSON.parse(sessionString); 79 await apiAgent.resumeSession(session); 80 81 // /*mass comment*/ console.log("Password-based session resumed successfully."); 82 setAgent(apiAgent); 83 setAuthMethod("password"); 84 setStatus("signedIn"); 85 setQuickAuth(apiAgent?.did || null); 86 return; 87 } 88 } catch (e) { 89 console.error("Failed to resume password-based session.", e); 90 localStorage.removeItem("sess"); 91 localStorage.removeItem("service"); 92 } 93 94 // /*mass comment*/ console.log("No active session found."); 95 setStatus("signedOut"); 96 setAgent(null); 97 setAuthMethod(null); 98 // do we want to null it here? 99 setQuickAuth(null); 100 }, [quickAuth, setQuickAuth]); 101 102 useEffect(() => { 103 const handleOAuthSessionDeleted = ( 104 event: CustomEvent<{ sub: string; cause: TokenRefreshError | TokenRevokedError | TokenInvalidError }>, 105 ) => { 106 console.error(`OAuth Session for ${event.detail.sub} was deleted.`, event.detail.cause); 107 setAgent(null); 108 setOauthSession(null); 109 setAuthMethod(null); 110 setStatus("signedOut"); 111 setQuickAuth(null); 112 }; 113 114 oauthClient.addEventListener("deleted", handleOAuthSessionDeleted as EventListener); 115 initialize(); 116 117 return () => { 118 oauthClient.removeEventListener("deleted", handleOAuthSessionDeleted as EventListener); 119 }; 120 }, [initialize, setQuickAuth]); 121 122 const loginWithPassword = async ( 123 user: string, 124 password: string, 125 service: string = "https://bsky.social", 126 ) => { 127 if (status !== "signedOut") return; 128 setStatus("loading"); 129 try { 130 let sessionData: AtpSessionData | undefined; 131 const apiAgent = new AtpAgent({ 132 service, 133 persistSession: (_evt, sess) => { 134 sessionData = sess; 135 }, 136 }); 137 await apiAgent.login({ identifier: user, password }); 138 139 if (sessionData) { 140 localStorage.setItem("service", service); 141 localStorage.setItem("sess", JSON.stringify(sessionData)); 142 setAgent(apiAgent); 143 setAuthMethod("password"); 144 setStatus("signedIn"); 145 setQuickAuth(apiAgent?.did || null); 146 // /*mass comment*/ console.log("Successfully logged in with password."); 147 } else { 148 throw new Error("Session data not persisted after login."); 149 } 150 } catch (e) { 151 console.error("Password login failed:", e); 152 setStatus("signedOut"); 153 setQuickAuth(null); 154 throw e; 155 } 156 }; 157 158 const loginWithOAuth = useCallback(async (handleOrPdsUrl: string) => { 159 if (status !== "signedOut") return; 160 try { 161 sessionStorage.setItem("postLoginRedirect", window.location.pathname + window.location.search); 162 await oauthClient.signIn(handleOrPdsUrl); 163 } catch (err) { 164 console.error("OAuth sign-in aborted or failed:", err); 165 } 166 }, [status]); 167 168 const logout = useCallback(async () => { 169 if (status !== "signedIn" || !agent) return; 170 setStatus("loading"); 171 172 try { 173 if (authMethod === "oauth" && oauthSession) { 174 await oauthClient.revoke(oauthSession.sub); 175 // /*mass comment*/ console.log("OAuth session revoked."); 176 } else if (authMethod === "password") { 177 localStorage.removeItem("service"); 178 localStorage.removeItem("sess"); 179 await (agent as AtpAgent).com.atproto.server.deleteSession(); 180 // /*mass comment*/ console.log("Password-based session deleted."); 181 } 182 } catch (e) { 183 console.error("Logout failed:", e); 184 } finally { 185 setAgent(null); 186 setAuthMethod(null); 187 setOauthSession(null); 188 setStatus("signedOut"); 189 setQuickAuth(null); 190 } 191 }, [status, agent, authMethod, oauthSession, setQuickAuth]); 192 193 return ( 194 <AuthContext 195 value={{ 196 agent, 197 status, 198 authMethod, 199 loginWithPassword, 200 loginWithOAuth, 201 logout, 202 }} 203 > 204 {children} 205 </AuthContext> 206 ); 207}; 208 209export const useAuth = () => use(AuthContext);